├── tests ├── __init__.py └── test_socialscan.py ├── MANIFEST.in ├── socialscan ├── __init__.py ├── __main__.py ├── util.py ├── cli.py └── platforms.py ├── demo ├── demo.gif ├── demo100.gif ├── demo_old.gif ├── demo.cast └── demo100.cast ├── renovate.json ├── pyproject.toml ├── .git-blame-ignore-revs ├── tox.ini ├── Pipfile ├── .gitignore ├── .travis.yml ├── setup.py ├── README.md ├── LICENSE └── Pipfile.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include tests/* -------------------------------------------------------------------------------- /socialscan/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0.1" 2 | -------------------------------------------------------------------------------- /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iojw/socialscan/HEAD/demo/demo.gif -------------------------------------------------------------------------------- /demo/demo100.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iojw/socialscan/HEAD/demo/demo100.gif -------------------------------------------------------------------------------- /demo/demo_old.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iojw/socialscan/HEAD/demo/demo_old.gif -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "group:allNonMajor", 5 | "schedule:monthly" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta:__legacy__" 4 | 5 | [tool.black] 6 | line-length = 100 -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Pass this file to --ignore-revs-file when executing `git blame` 2 | 3 | # Migrate code style to Black 4 | d2759a97e3bf879487ebdb1690e9cc2b414af5de -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py39 3 | skipdist = true 4 | 5 | [testenv] 6 | passenv = TRAVIS 7 | deps = 8 | pytest 9 | pytest-timeout 10 | pytest-xdist 11 | pytest-rerunfailures 12 | commands = pytest -v -n auto --reruns 1 --reruns-delay 10 --log-level DEBUG -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | socialscan = {editable = true, path = "."} 8 | 9 | [dev-packages] 10 | tox = "*" 11 | flake8 = "*" 12 | black = "==23.12.1" 13 | pytest = "*" 14 | 15 | [requires] 16 | python_version = "3.9" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Project files 6 | *.sublime-workspace 7 | *.sublime-project 8 | .idea 9 | .vscode 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # Unit test / coverage reports 33 | htmlcov/ 34 | .tox/ 35 | .nox/ 36 | .coverage 37 | .coverage.* 38 | .cache 39 | nosetests.xml 40 | coverage.xml 41 | *.cover 42 | .hypothesis/ 43 | .pytest_cache/ 44 | 45 | # Environments 46 | .env 47 | .venv 48 | env/ 49 | venv/ 50 | ENV/ 51 | env.bak/ 52 | venv.bak/ 53 | 54 | # Files for script debugging 55 | info/ 56 | deploy_to_pypi.sh 57 | 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | python: 4 | - '3.9' 5 | install: 6 | - python setup.py install 7 | - pip install tox-travis 8 | script: 9 | - tox --recreate 10 | deploy: 11 | provider: pypi 12 | skip_existing: true 13 | user: iojw 14 | on: 15 | tags: true 16 | distributions: sdist bdist_wheel 17 | password: 18 | secure: cKBb9sZm0aO4pujI3do+yiRFk4UDRLdwcOBESv3oGMoWhY/n4zK+QnmJRKRQbm7vmkegdDFBRY3lUU9/M6L1DgVzNalCpaORcx9LfOELhg8cL7IoOn2zrDg2sAW2QeZgZ/PtB7+wvzJ5GFoQz6BDYy/oXQ1jruo3IPR/w44iXxMrAWI9lP6G29mE1uDUYc1tGFrLte12mdigRKxBiF9pyXE+Pe5MhLk8lzz1Gi0zi691A+Hx9PCrSEMRdO9UxtwnmEdcQSQuV+yw8KOxeMyhCly7edX07b2Gw55VQYhUYFzHSaoZ/NA1kTogUYXsOpwbhakoaEKfwABHy7h+lbZfObRubQcFiOHGHqNCpi76OcoVpEtMcin9urUyV0Sw3YPOHro1yR3aY5KiW+Ch6FddGHks8LgnqoFnDweespHKN6k6IYy2enaHZHlkMMHjeebw6GV13otwGvTyKBav1SURthWN6FmLsm9tWUyUFIKoiGQ/RK3ZITW0S6/7B2W6nI09REyylv/a/1EnZsb9qEtlIBHZ0drJi8DFKqalzvWZ17V5XUk4skd95FZNxzRlm1nZy/f71emgtuPG9qKrwvPZ6/oYPB7xrtuQEuUyIr8S2m8yykiL1IYzorkTSwj3YXV7pZNiTHQCa77S9kLjqUsRXpMP8/fQV1mvvzqEKSpL/68= 19 | -------------------------------------------------------------------------------- /socialscan/__main__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import asyncio 6 | import sys 7 | 8 | from socialscan import cli 9 | 10 | 11 | def main(): 12 | # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp 13 | if sys.platform.startswith("win") and sys.version_info >= (3, 8): 14 | try: 15 | from asyncio import WindowsSelectorEventLoopPolicy 16 | except ImportError: 17 | pass 18 | else: 19 | if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy): 20 | asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) 21 | if sys.version_info >= (3, 7): 22 | asyncio.run(cli.main()) 23 | else: 24 | loop = asyncio.get_event_loop() 25 | loop.run_until_complete(cli.main()) 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from setuptools import setup 4 | 5 | from socialscan import __version__ 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | install_requires = [ 10 | 'dataclasses;python_version<"3.7"', 11 | "colorama", 12 | "aiohttp>=3.5.0", 13 | "tqdm>=4.31.0", 14 | ] 15 | 16 | tests_requires = ["tox", "flake8"] 17 | 18 | with open(path.join(here, "README.md"), encoding="utf-8") as f: 19 | long_description = f.read() 20 | 21 | setup( 22 | name="socialscan", 23 | version=__version__, 24 | description="Open-source intelligence tool for checking email address and username usage on online platforms", 25 | long_description=long_description, 26 | long_description_content_type="text/markdown", 27 | url="https://github.com/iojw/socialscan", 28 | author="Isaac Ong", 29 | author_email="isaacong.jw@gmail.com", 30 | classifiers=[ 31 | "Development Status :: 4 - Beta", 32 | "Framework :: AsyncIO", 33 | "Environment :: Console", 34 | "Operating System :: OS Independent", 35 | "Topic :: Utilities", 36 | "License :: OSI Approved :: MIT License", 37 | "Programming Language :: Python :: 3.6", 38 | "Programming Language :: Python :: 3.7", 39 | "Programming Language :: Python :: 3.8", 40 | ], 41 | keywords="email email-checker username username-checker social-media", 42 | packages=["socialscan"], 43 | python_requires=">=3.6", 44 | install_requires=install_requires, 45 | extras_require={"tests": install_requires + tests_requires}, 46 | entry_points={"console_scripts": ["socialscan=socialscan.__main__:main"]}, 47 | ) 48 | -------------------------------------------------------------------------------- /tests/test_socialscan.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | 5 | from socialscan.platforms import PlatformResponse, Platforms 6 | from socialscan.util import sync_execute_queries 7 | 8 | TIMEOUT_DURATION = 25 # in seconds 9 | 10 | AVAILABLE_USERNAMES = ["jsndiwimw"] 11 | UNAVAILABLE_USERNAMES = ["social"] 12 | INVALID_USERNAMES = ["*"] 13 | 14 | UNUSED_EMAILS = ["unused@notanemail.com"] 15 | USED_EMAILS = ["fire@gmail.com"] 16 | 17 | logging.basicConfig(level=logging.DEBUG) 18 | 19 | 20 | def assert_available(response: PlatformResponse): 21 | assert response.available 22 | assert response.valid 23 | assert response.success 24 | 25 | 26 | def assert_unavailable(response: PlatformResponse): 27 | assert not response.available 28 | assert response.valid 29 | assert response.success 30 | 31 | 32 | def assert_invalid(response: PlatformResponse): 33 | assert not response.available 34 | assert not response.valid 35 | assert response.success 36 | 37 | 38 | @pytest.mark.parametrize("platform", [p for p in Platforms if hasattr(p.value, "check_username")]) 39 | @pytest.mark.parametrize( 40 | "usernames, assert_function", 41 | [ 42 | (AVAILABLE_USERNAMES, assert_available), 43 | (UNAVAILABLE_USERNAMES, assert_unavailable), 44 | (INVALID_USERNAMES, assert_invalid), 45 | ], 46 | ) 47 | @pytest.mark.timeout(TIMEOUT_DURATION) 48 | def test_usernames(platform, usernames, assert_function): 49 | for username in usernames: 50 | response = sync_execute_queries([username], [platform])[0] 51 | assert_function(response) 52 | 53 | 54 | @pytest.mark.parametrize("platform", [p for p in Platforms if hasattr(p.value, "check_email")]) 55 | @pytest.mark.parametrize( 56 | "emails, assert_function", 57 | [(UNUSED_EMAILS, assert_available), (USED_EMAILS, assert_unavailable)], 58 | ) 59 | @pytest.mark.timeout(TIMEOUT_DURATION) 60 | def test_emails(platform, emails, assert_function): 61 | for email in emails: 62 | response = sync_execute_queries([email], [platform])[0] 63 | assert_function(response) 64 | -------------------------------------------------------------------------------- /demo/demo.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 100, "height": 30, "timestamp": 1553426711, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}} 2 | [0.123977, "o", "\u001b[01;32miojw@io-comp\u001b[00m:\u001b[01;34msocialscan\u001b[00m$ "] 3 | [1.049906, "o", "socialscan username-2 email74@gmail.com"] 4 | [2.911456, "o", "\r\n"] 5 | [3.554253, "o", "\r 0%| | 0/22 [0.00s]\u001b[0m"] 6 | [3.886323, "o", "\r 27%|████████▍ | 6/22 [0.33s]\u001b[0m"] 7 | [4.148449, "o", "\r 55%|████████████████▎ | 12/22 [0.59s]\u001b[0m"] 8 | [4.284777, "o", "\r 64%|███████████████████ | 14/22 [0.73s]\u001b[0m"] 9 | [4.589991, "o", "\r 77%|███████████████████████▏ | 17/22 [1.04s]\u001b[0m"] 10 | [5.200081, "o", "\r 86%|█████████████████████████▉ | 19/22 [1.64s]\u001b[0m"] 11 | [5.490334, "o", "\r 95%|████████████████████████████▋ | 21/22 [1.94s]\u001b[0m"] 12 | [5.534051, "o", "\u001b[0m\r \u001b[0m\r----------------------------------------\r\n \u001b[1musername-2\u001b[0m\r\n----------------------------------------\u001b[0m\r\n\u001b[0m\u001b[92mGitLab\u001b[0m\r\n\u001b[0m\u001b[92mLastfm\u001b[0m\r\n\u001b[0m\u001b[92mPastebin\u001b[0m\r\n\u001b[0m\u001b[33mGitHub\u001b[0m\r\n\u001b[0m\u001b[33mReddit\u001b[0m\r\n\u001b[0m\u001b[33mSnapchat\u001b[0m\r\n\u001b[0m\u001b[33mTumblr\u001b[0m\r\n\u001b[0m\u001b[36mInstagram: \u001b[37mUsernames can only use letters, numbers, underscores and periods.\u001b[0m\r\n\u001b[0m\u001b[36mTwitter: \u001b[37mYour username can only contain letters, numbers and '_'\u001b[0m\r\n\u001b[0m----------------------------------------\r\n \u001b[1memail74@gmail.com\u001b[0m\r\n----------------------------------------\u001b[0m"] 13 | [5.536106, "o", "\r\n\u001b[0m\u001b[92mGitHub\u001b[0m\r\n"] 14 | [5.546565, "o", "\u001b[0m\u001b[92mLastfm\u001b[0m\r\n\u001b[0m\u001b[92mPastebin\u001b[0m\r\n\u001b[0m\u001b[92mPinterest\u001b[0m\r\n\u001b[0m\u001b[33mInstagram\u001b[0m\r\n\u001b[0m\u001b[33mSpotify"] 15 | [5.549527, "o", "\u001b[0m\r\n\u001b[0m\u001b[33mTumblr\u001b[0m\r\n\u001b[0m\u001b[33mTwitter\u001b[0m\r\n"] 16 | [5.556328, "o", "\u001b[0m\r\n\u001b[0m\u001b[92mAvailable, \u001b[0m\u001b[0m\u001b[33mTaken/Reserved, \u001b[0m\u001b[0m\u001b[36mInvalid, \u001b[0m\u001b[0m\u001b[31mError\u001b[0m\r\n\u001b[0mCompleted 17 queries in 2.01s\u001b[0m\r\n"] 17 | [5.566521, "o", "\u001b[0m"] 18 | [5.569134, "o", "\u001b[0m"] 19 | [5.575266, "o", "\u001b[0m"] 20 | [5.639338, "o", "\u001b[01;32miojw@io-comp\u001b[00m:\u001b[01;34msocialscan\u001b[00m$ "] 21 | [9.214143, "o", "exit\r\n"] 22 | -------------------------------------------------------------------------------- /socialscan/util.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import asyncio 6 | import re 7 | import sys 8 | 9 | import aiohttp 10 | 11 | from socialscan.platforms import (EmailQueryable, PlatformResponse, Platforms, 12 | PrerequestRequired, QueryError, 13 | UsernameQueryable) 14 | 15 | EMAIL_REGEX = re.compile( 16 | r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?)+$" 17 | ) 18 | 19 | 20 | async def init_prerequest(platform, checkers): 21 | if issubclass(platform.value, PrerequestRequired): 22 | await checkers[platform].get_token() 23 | 24 | 25 | def init_checkers(session, platforms=list(Platforms), proxy_list=[]): 26 | checkers = {} 27 | for platform in platforms: 28 | checkers[platform] = platform.value(session, proxy_list=proxy_list) 29 | return checkers 30 | 31 | 32 | async def query(query_, platform, checkers): 33 | try: 34 | is_email = EMAIL_REGEX.match(query_) 35 | if is_email and issubclass(platform.value, EmailQueryable): 36 | response = await checkers[platform].check_email(query_) 37 | if response is None: 38 | raise QueryError("Error retrieving result") 39 | return response 40 | elif not is_email and issubclass(platform.value, UsernameQueryable): 41 | response = await checkers[platform].check_username(query_) 42 | if response is None: 43 | raise QueryError("Error retrieving result") 44 | return response 45 | except (aiohttp.ClientError, KeyError, QueryError) as e: 46 | return PlatformResponse( 47 | platform=platform, 48 | query=query_, 49 | available=False, 50 | valid=False, 51 | success=False, 52 | message=f"{type(e).__name__} - {e}", 53 | link=None, 54 | ) 55 | 56 | 57 | async def execute_queries(queries, platforms=list(Platforms), proxy_list=[]): 58 | """Execute each of the queries on the specified platforms concurrently and return a list of results. 59 | 60 | Args: 61 | queries (`list` of `str`): List of queries to search. 62 | platforms (`list` of `Platform` members, optional): List of platforms to execute queries for. Defaults to all platforms. 63 | proxy_list (`list` of `str`, optional): List of HTTP proxies to execute queries with. 64 | 65 | Returns: 66 | `list` of `PlatformResponse` objects in the same order as the list of queries and platforms passed. 67 | """ 68 | async with aiohttp.ClientSession() as session: 69 | checkers = init_checkers(session, platforms=platforms, proxy_list=proxy_list) 70 | query_tasks = [query(q, p, checkers) for q in queries for p in platforms] 71 | results = await asyncio.gather(*query_tasks) 72 | return [x for x in results if x is not None] 73 | 74 | 75 | def sync_execute_queries(queries, platforms=list(Platforms), proxy_list=[]): 76 | """Execute each of the queries on the specified platforms concurrently and return a list of results. Synchronous wrapper around `execute_queries` 77 | 78 | Args: 79 | queries (`list` of `str`): List of queries to search. 80 | platforms (`list` of `Platforms` members, optional): List of platforms to execute queries for. Defaults to all platforms. 81 | proxy_list (`list` of `str`, optional): List of HTTP proxies to execute queries with. 82 | 83 | Returns: 84 | `list` of `PlatformResponse` objects in the same order as the list of queries and platforms passed. 85 | """ 86 | if sys.version_info >= (3, 7): 87 | return asyncio.run(execute_queries(queries, platforms, proxy_list)) 88 | else: 89 | loop = asyncio.get_event_loop() 90 | return loop.run_until_complete(execute_queries(queries, platforms, proxy_list)) 91 | -------------------------------------------------------------------------------- /demo/demo100.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 100, "height": 30, "timestamp": 1554042636, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}} 2 | [0.187449, "o", "\u001b[01;32miojw@io-comp\u001b[00m:\u001b[01;34msocialscan\u001b[00m$ "] 3 | [1.783863, "o", "socialscan --platform tumblr --input 100_queries.txt --view-by platform"] 4 | [3.286382, "o", "\r\n"] 5 | [4.498113, "o", "\r 0%| | 0/100 [0.00s]\u001b[0m"] 6 | [7.118299, "o", "\r 1%|▎ | 1/100 [2.62s]\u001b[0m"] 7 | [7.247785, "o", "\r 7%|██ | 7/100 [2.75s]"] 8 | [7.249268, "o", "\u001b[0m"] 9 | [7.349479, "o", "\r 19%|█████▌ | 19/100 [2.85s]\u001b[0m"] 10 | [7.482715, "o", "\r 34%|█████████▊ | 34/100 [2.98s]\u001b[0m"] 11 | [7.583697, "o", "\r 42%|████████████▏ | 42/100 [3.09s]\u001b[0m"] 12 | [7.683809, "o", "\r 57%|████████████████▌ | 57/100 [3.19s]\u001b[0m"] 13 | [7.828384, "o", "\r 89%|█████████████████████████▊ | 89/100 [3.33s]\u001b[0m"] 14 | [8.461236, "o", "\u001b[0m\r \u001b[0m"] 15 | [8.464288, "o", "\r----------------------------------------\r\n \u001b[1mTumblr\u001b[0m\r\n----------------------------------------\u001b[0m\r\n\u001b[0m\u001b[92mallie2407k\u001b[0m\r\n\u001b[0m\u001b[92mamajlijad\u001b[0m\r\n\u001b[0m\u001b[92mannazizj\u001b[0m\r\n\u001b[0m\u001b[92manonyme81a\u001b[0m\r\n\u001b[0m\u001b[92maol3ig"] 16 | [8.467437, "o", "\u001b[0m\r\n\u001b[0m\u001b[92mapatijomi\u001b[0m\r\n\u001b[0m\u001b[92maxiorurninOd\u001b[0m\r\n\u001b[0m\u001b[92mazntwkoolguyx\u001b[0m\r\n\u001b[0m\u001b[92mbeduimelp\u001b[0m\r\n\u001b[0m\u001b[92mbitterden\u001b[0m\r\n\u001b[0m\u001b[92mburudam\u001b[0m\r\n\u001b[0m\u001b[92mcapaciteoz\u001b[0m\r\n\u001b[0m\u001b[92mcatantt\u001b[0m\r\n\u001b[0m\u001b[92mcolutzek1966n\u001b[0m\r\n\u001b[0m\u001b[92mcrapularej\u001b[0m\r\n\u001b[0m\u001b[92mcrazymelonp\u001b[0m\r\n\u001b[0m\u001b[92mdarovanjeq\u001b[0m\r\n\u001b[0m\u001b[92mderekouyangh\u001b[0m\r\n\u001b[0m\u001b[92mdiametrass\u001b[0m\r\n\u001b[0m\u001b[92mdjarfario\u001b[0m\r\n\u001b[0m\u001b[92meledgedoxm\u001b[0m\r\n\u001b[0m\u001b[92mempondatzf\u001b[0m\r\n\u001b[0m\u001b[92mesglajarf\u001b[0m\r\n\u001b[0m\u001b[92mestopadash\u001b[0m\r\n\u001b[0m\u001b[92mexpresion2hd\u001b[0m\r\n\u001b[0m\u001b[92meyiswau\u001b[0m\r\n\u001b[0m\u001b[92mEzzinij\u001b[0m\r\n\u001b[0m\u001b[92mffurfiolz\u001b[0m\r\n\u001b[0m\u001b[92mFilmfachm\u001b[0m\r\n\u001b[0m\u001b[92mfilozofuh"] 17 | [8.47343, "o", "\u001b[0m\r\n\u001b[0m\u001b[92mgarranhons\u001b[0m\r\n\u001b[0m\u001b[92mGaststubef\u001b[0m\r\n\u001b[0m\u001b[92mgewundertn\u001b[0m\r\n\u001b[0m\u001b[92mgyi1keg\u001b[0m\r\n\u001b[0m\u001b[92mHeizrohrp\u001b[0m\r\n\u001b[0m\u001b[92mHeldenmutp\u001b[0m\r\n\u001b[0m\u001b[92mhobijimaz\u001b[0m\r\n\u001b[0m\u001b[92mHypostasee\u001b[0m\r\n\u001b[0m\u001b[92mi2l1alkj\u001b[0m\r\n\u001b[0m\u001b[92miherutsej\u001b[0m\r\n\u001b[0m\u001b[92millseeuinhellz\u001b[0m\r\n\u001b[0m\u001b[92minabituauh\u001b[0m\r\n\u001b[0m\u001b[92mKobulilin\u001b[0m\r\n\u001b[0m\u001b[92mkrajnikan\u001b[0m\r\n\u001b[0m\u001b[92mkresselz\u001b[0m\r\n\u001b[0m\u001b[92mKurnickik\u001b[0m\r\n\u001b[0m\u001b[92mllithrz\u001b[0m\r\n\u001b[0m\u001b[92mloqymhhx\u001b[0m\r\n\u001b[0m\u001b[92mmaapoyb\u001b[0m\r\n\u001b[0m\u001b[92mmcpalestinei\u001b[0m\r\n\u001b[0m\u001b[92mmikadolla\u001b[0m\r\n\u001b[0m\u001b[92mMipimor\u001b[0m\r\n\u001b[0m\u001b[92mmotenihb\u001b[0m\r\n\u001b[0m\u001b[92mmuntmetera\u001b[0m\r\n\u001b[0m\u001b[92mn4tuts\u001b[0m\r\n\u001b[0m\u001b[92mneuritiesg\u001b[0m\r\n\u001b[0m\u001b[92mNeyirobhul\u001b[0m\r\n\u001b[0m\u001b[92mNjivercahy\u001b[0m\r\n\u001b[0m\u001b[92mokorkowyn\u001b[0m\r\n\u001b[0m\u001b[92mPantozzio\u001b[0m\r\n\u001b[0m\u001b[92mparauletac\u001b[0m\r\n\u001b[0m\u001b[92mperemarquinaf\u001b[0m\r\n\u001b[0m\u001b[92mprimarcq\u001b[0m\r\n\u001b[0m\u001b[92mradarowyh\u001b[0m\r\n\u001b[0m\u001b[92mrebrodesz\u001b[0m\r\n\u001b[0m\u001b[92mrevellinob\u001b[0m\r\n\u001b[0m\u001b[92mrickiebaby14q\u001b[0m\r\n\u001b[0m\u001b[92mRistgriffy\u001b[0m\r\n\u001b[0m\u001b[92mrondhoutd\u001b[0m\r\n\u001b[0m\u001b[92mRovaveipiedt\u001b[0m\r\n"] 18 | [8.503381, "o", "\u001b[0m\u001b[92mrowdyisma\u001b[0m\r\n\u001b[0m\u001b[92mrozesejiy\u001b[0m\r\n\u001b[0m\u001b[92msahsteabhert\u001b[0m\r\n\u001b[0m\u001b[92msatwant09f\u001b[0m\r\n\u001b[0m\u001b[92mshotadast\u001b[0m\r\n\u001b[0m\u001b[92msongintheairg\u001b[0m\r\n\u001b[0m\u001b[92msorcaism\u001b[0m\r\n\u001b[0m\u001b[92msoslogarr\u001b[0m\r\n\u001b[0m\u001b[92mspeurdem\u001b[0m\r\n\u001b[0m\u001b[92mSpornitzq\u001b[0m\r\n\u001b[0m\u001b[92msvinutih\u001b[0m\r\n\u001b[0m\u001b[92mSzaradowoc\u001b[0m\r\n\u001b[0m\u001b[92mterenom\u001b[0m\r\n\u001b[0m\u001b[92mtroficznyh\u001b[0m\r\n\u001b[0m\u001b[92mupdaphfautuapv\u001b[0m\r\n\u001b[0m\u001b[92mVeiplernWenk\u001b[0m\r\n\u001b[0m\u001b[92mVollzugn\u001b[0m\r\n\u001b[0m\u001b[92mzewDitteegOp\u001b[0m\r\n\u001b[0m\u001b[92mzumuteni\u001b[0m\r\n\u001b[0m\u001b[33mdog12\u001b[0m\r\n\u001b[0m\u001b[33melin\u001b[0m\r\n\u001b[0m\u001b[33mirfank\u001b[0m\r\n\u001b[0m\u001b[33mjonathan1\u001b[0m\r\n\u001b[0m\u001b[33mkalvisp\u001b[0m\r\n\u001b[0m\u001b[33mmysecondlifex\u001b[0m\r\n\u001b[0m\u001b[33mplopperr\u001b[0m\r\n\u001b[0m\u001b[33mSaffery\u001b[0m\r\n\u001b[0m\u001b[33msearry\u001b[0m\r\n\u001b[0m\u001b[33mSpanuh\u001b[0m\r\n\u001b[0m\u001b[33mstifg\u001b[0m\r\n"] 19 | [8.552006, "o", "\u001b[0m\r\n\u001b[92mAvailable, \u001b[0m\u001b[0m\u001b[33mTaken/Reserved, \u001b[0m"] 20 | [8.55477, "o", "\u001b[0m\u001b[36mInvalid, \u001b[0m\u001b[0m\u001b[31mError\u001b[0m\r\n\u001b[0mCompleted 100 queries in 4.07s\u001b[0m\r\n"] 21 | [8.675768, "o", "\u001b[0m\u001b[0m"] 22 | [8.678647, "o", "\u001b[0m"] 23 | [8.780379, "o", "\u001b[01;32miojw@io-comp\u001b[00m:\u001b[01;34msocialscan\u001b[00m$ "] 24 | [12.372332, "o", "exit\r\n"] 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # socialscan 2 | [![Build Status](https://travis-ci.com/iojw/socialscan.svg?token=4yLRbSuqAQqrjanbzeXs&branch=master)](https://travis-ci.com/iojw/socialscan) 3 | [![Downloads](https://pepy.tech/badge/socialscan)](https://pepy.tech/project/socialscan/) 4 | [![MPL 2.0 license](https://img.shields.io/badge/License-MPL%202.0-blue.svg)](https://www.mozilla.org/en-US/MPL/2.0/) 5 | [![Python 3.6+](https://img.shields.io/badge/python-3.6+-green.svg)](https://www.python.org/downloads/) 6 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 7 | 8 | socialscan offers **accurate** and **fast** checks for email address and username usage on online platforms. 9 | 10 | Given an email address or username, socialscan returns whether it is available, taken or invalid on online platforms. 11 | 12 | Features that differentiate socialscan from similar tools (e.g. knowem.com, Namechk, and Sherlock): 13 | 14 | 1. **100% accuracy**: socialscan's query method eliminates the false positives and negatives that often occur in similar tools, ensuring that results are always accurate. 15 | 16 | 2. **Speed**: socialscan uses [asyncio](https://docs.python.org/3/library/asyncio.html) along with [aiohttp](https://aiohttp.readthedocs.io/en/stable/) to conduct all queries concurrently, providing fast searches even with bulk queries involving hundreds of usernames and email addresses. On a test computer with average specs and Internet speed, 100 queries were executed in ~4 seconds. 17 | 18 | 3. **Library / CLI**: socialscan can be executed through a CLI, or imported as a Python library to be used with existing code. 19 | 20 | 4. **Email support**: socialscan supports queries for both email addresses and usernames. 21 | 22 | The following platforms are currently supported: 23 | 24 | | | Username | Email | 25 | |:---------:|:--------:|:--------:| 26 | | Instagram | ✔️ | ✔️ | 27 | | Twitter | ✔️ | ✔️ | 28 | | GitHub | ✔️ | ✔️ | 29 | | Tumblr | ✔️ | ✔️ | 30 | | Lastfm | ✔️ | ✔️ | 31 | | Snapchat | ✔️ | | 32 | | GitLab | ✔️ | | 33 | | Reddit | ✔️ | | 34 | | Yahoo | ✔️ | | 35 | | Pinterest | | ✔️ | 36 | | Firefox | | ✔️ | 37 | 38 | ![](https://github.com/iojw/socialscan/raw/master/demo/demo.gif) 39 | ![](https://github.com/iojw/socialscan/raw/master/demo/demo100.gif) 40 | 41 | ## Background 42 | 43 | Other similar tools check username availability by requesting the profile page of the username in question and based on information like the HTTP status code or error text on the requested page, determine whether a username is already taken. This is a naive approach that fails in the following cases: 44 | 45 | - Reserved keywords: Most platforms have a set of keywords that they don't allow to be used in usernames 46 | (A simple test: try checking reserved words like 'admin' or 'home' or 'root' and see if other services mark them as available) 47 | 48 | - Deleted/banned accounts: Deleted/banned account usernames tend to be unavailable even though the profile pages might not exist 49 | 50 | Therefore, these tools tend to come up with false positives and negatives. This method of checking is also dependent on platforms having web-based profile pages and cannot be extended to email addresses. 51 | 52 | socialscan aims to plug these gaps by directly querying the registration servers of the platforms instead, retrieving the appropriate CSRF tokens, headers, and cookies. 53 | 54 | ## Installation 55 | 56 | ### pip 57 | ``` 58 | > pip install socialscan 59 | ``` 60 | 61 | ### Install from source 62 | ``` 63 | > git clone https://github.com/iojw/socialscan.git 64 | > cd socialscan 65 | > pip install . 66 | ``` 67 | 68 | ## Usage 69 | ``` 70 | usage: socialscan [list of usernames/email addresses to check] 71 | 72 | optional arguments: 73 | -h, --help show this help message and exit 74 | --platforms [platform [platform ...]], -p [platform [platform ...]] 75 | list of platforms to query (default: all platforms) 76 | --view-by {platform,query} 77 | view results sorted by platform or by query (default: 78 | query) 79 | --available-only, -a only print usernames/email addresses that are 80 | available and not in use 81 | --cache-tokens, -c cache tokens for platforms requiring more than one 82 | HTTP request (Snapchat, GitHub, Instagram. Lastfm & 83 | Tumblr), reducing total number of requests sent 84 | --input input.txt, -i input.txt 85 | file containg list of queries to execute 86 | --proxy-list proxy_list.txt 87 | file containing list of HTTP proxy servers to execute 88 | queries with 89 | --verbose, -v show query responses as they are received 90 | --show-urls display profile URLs for usernames on supported platforms 91 | (profiles may not exist if usernames are reserved or belong to deleted/banned accounts) 92 | --json json.txt output results in JSON format to the specified file 93 | --version show program's version number and exit 94 | ``` 95 | 96 | ## As a library 97 | socialscan can also be imported into existing code and used as a library. 98 | 99 | v1.0.0 introduces the async method `execute_queries` and the corresponding synchronous wrapper `sync_execute_queries` that takes a list of queries and optional list of platforms and proxies, executing all queries concurrently. The method then returns a list of results in the same order. 100 | 101 | ```python 102 | from socialscan.util import Platforms, sync_execute_queries 103 | 104 | queries = ["username1", "email2@gmail.com", "mail42@me.com"] 105 | platforms = [Platforms.GITHUB, Platforms.LASTFM] 106 | results = sync_execute_queries(queries, platforms) 107 | for result in results: 108 | print(f"{result.query} on {result.platform}: {result.message} (Success: {result.success}, Valid: {result.valid}, Available: {result.available})") 109 | ``` 110 | Output: 111 | ``` 112 | username1 on GitHub: Username is already taken (Success: True, Valid: True, Available: False) 113 | username1 on Lastfm: Sorry, this username isn't available. (Success: True, Valid: True, Available: False) 114 | email2@gmail.com on GitHub: Available (Success: True, Valid: True, Available: True) 115 | email2@gmail.com on Lastfm: Sorry, that email address is already registered to another account. (Success: True, Valid: True, Available: False) 116 | mail42@me.com on GitHub: Available (Success: True, Valid: True, Available: True) 117 | mail42@me.com on Lastfm: Looking good! (Success: True, Valid: True, Available: True) 118 | ``` 119 | 120 | ## Text file input 121 | For bulk queries with the `--input` option, place one username/email on each line in the .txt file: 122 | ``` 123 | username1 124 | email2@mail.com 125 | username3 126 | ``` 127 | 128 | ## Donations 129 | 130 | If you find this tool useful and would like to support its continued development, you can donate here. Thank you for your support. 131 | 132 | [![Donate Via PayPal](https://www.paypal.com/en_US/i/btn/btn_donate_LG.gif)](https://paypal.me/isaacong) 133 | 134 | BTC: bc1qwrnukyc6xh9aygu5ps2geh8stmvagsj5u4v7j6 135 | ETH: 0x45a0F91666391078eA521A6123559E49DAb1275f 136 | 137 | ## Contributing 138 | Errors, suggestions or want a site added? [Submit an issue](https://github.com/iojw/socialscan/issues). 139 | 140 | PRs are always welcome 🙂 141 | 142 | Please ensure that the code is formatted with `black` using the config in `pyproject.toml` before submitting a PR. 143 | 144 | ## License 145 | MPL 2.0 146 | -------------------------------------------------------------------------------- /socialscan/cli.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import argparse 6 | import asyncio 7 | import json 8 | import logging 9 | import sys 10 | import time 11 | from collections import defaultdict, namedtuple 12 | from dataclasses import asdict 13 | from operator import attrgetter 14 | 15 | import aiohttp 16 | import colorama 17 | import tqdm 18 | from colorama import Fore, Style 19 | 20 | from socialscan import __version__ 21 | from socialscan.platforms import PlatformResponse, Platforms 22 | from socialscan.util import init_checkers, init_prerequest, query 23 | 24 | BAR_WIDTH = 50 25 | BAR_FORMAT = "{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed_s:.2f}s]" 26 | 27 | DIVIDER_LENGTH = 40 28 | 29 | Colour = namedtuple("Colour", ["Primary", "Secondary"]) 30 | COLOUR_AVAILABLE = Colour(Fore.LIGHTGREEN_EX, Fore.LIGHTGREEN_EX) 31 | COLOUR_UNAVAILABLE = Colour(Fore.YELLOW, Fore.WHITE) 32 | COLOUR_INVALID = Colour(Fore.CYAN, Fore.WHITE) 33 | COLOUR_ERROR = Colour(Fore.RED, Fore.RED) 34 | 35 | 36 | def init_parser(): 37 | parser = argparse.ArgumentParser( 38 | description="Command-line interface for checking email address and username usage on online platforms: " 39 | + ", ".join(p.value.__name__ for p in Platforms) 40 | ) 41 | parser.add_argument( 42 | "queries", 43 | metavar="query", 44 | nargs="*", 45 | help="one or more usernames/email addresses to query (email addresses are automatically be queried if they match the format)", 46 | ) 47 | parser.add_argument( 48 | "--platforms", 49 | "-p", 50 | metavar="platform", 51 | nargs="*", 52 | help="list of platforms to query " "(default: all platforms)", 53 | ) 54 | parser.add_argument( 55 | "--view-by", 56 | dest="view_key", 57 | choices=["platform", "query"], 58 | default="query", 59 | help="view results sorted by platform or by query (default: query)", 60 | ) 61 | parser.add_argument( 62 | "--available-only", 63 | "-a", 64 | action="store_true", 65 | help="only print usernames/email addresses that are available and not in use", 66 | ) 67 | parser.add_argument( 68 | "--cache-tokens", 69 | "-c", 70 | action="store_true", 71 | help="cache tokens for platforms requiring more than one HTTP request (Snapchat, GitHub, Instagram. Lastfm, Tumblr & Yahoo), reducing total number of requests sent", 72 | ) 73 | parser.add_argument( 74 | "--input", "-i", metavar="input.txt", help="file containg list of queries to execute" 75 | ) 76 | parser.add_argument( 77 | "--proxy-list", 78 | metavar="proxy_list.txt", 79 | help="file containing list of HTTP proxy servers to execute queries with", 80 | ) 81 | parser.add_argument( 82 | "--verbose", "-v", action="store_true", help="show query responses as they are received" 83 | ) 84 | parser.add_argument( 85 | "--show-urls", 86 | action="store_true", 87 | help="display profile URLs for usernames on supported platforms (profiles may not exist if usernames are reserved or belong to deleted/banned accounts)", 88 | ) 89 | parser.add_argument( 90 | "--json", 91 | metavar="json.txt", 92 | help="output results in JSON format to the specified file", 93 | ) 94 | parser.add_argument( 95 | "--debug", 96 | action="store_true", 97 | help="output debug messages", 98 | ) 99 | parser.add_argument("--version", version=f"%(prog)s {__version__}", action="version") 100 | return parser 101 | 102 | 103 | def pretty_print(results, *, view_value, available_only, show_urls): 104 | for key, responses in results.items(): 105 | if available_only and not [r for r in responses if r.available]: 106 | continue 107 | 108 | header = ( 109 | f"{'-' * DIVIDER_LENGTH}\n" 110 | f"{' ' * (DIVIDER_LENGTH // 2 - len(key) // 2) + Style.BRIGHT + str(key) + Style.RESET_ALL}\n" 111 | f"{'-' * DIVIDER_LENGTH}" 112 | ) 113 | print(header) 114 | 115 | responses.sort(key=lambda response: str(getattr(response, view_value)).lower()) 116 | responses.sort(key=attrgetter("available", "valid", "success"), reverse=True) 117 | for response in responses: 118 | value = str(getattr(response, view_value)) 119 | if available_only and not response.available: 120 | continue 121 | if not response.success: 122 | print(COLOUR_ERROR.Primary + f"{value}: {response.message}", file=sys.stderr) 123 | elif not response.valid: 124 | print( 125 | COLOUR_INVALID.Primary 126 | + f"{value}: {COLOUR_INVALID.Secondary}{response.message}" 127 | ) 128 | else: 129 | col = COLOUR_AVAILABLE if response.available else COLOUR_UNAVAILABLE 130 | result_text = col.Primary + value 131 | if response.link and show_urls: 132 | result_text += col.Secondary + f" - {response.link}" 133 | print(result_text) 134 | 135 | print("\n" + COLOUR_AVAILABLE.Primary + "Available, ", end="") 136 | print(COLOUR_UNAVAILABLE.Primary + "Taken/Reserved, ", end="") 137 | print(COLOUR_INVALID.Primary + "Invalid, ", end="") 138 | print(COLOUR_ERROR.Primary + "Error") 139 | 140 | 141 | def print_json(results, *, file, available_only): 142 | if available_only: 143 | results = {key: [v for v in values if v.available] for key, values in results.items()} 144 | 145 | def serialize(obj): 146 | if isinstance(obj, PlatformResponse): 147 | # Omit None and convert Platform objects to str 148 | return asdict( 149 | obj, 150 | dict_factory=lambda data: dict( 151 | [(x[0], str(x[1])) for x in data if x[1] is not None] 152 | ), 153 | ) 154 | 155 | with open(file, "w") as f: 156 | f.write(json.dumps(results, default=serialize, indent=4)) 157 | 158 | 159 | async def main(): 160 | start_time = time.time() 161 | colorama.init(autoreset=True) 162 | if sys.version_info >= (3, 7): 163 | sys.stdout.reconfigure(encoding="utf-8") 164 | parser = init_parser() 165 | args = parser.parse_args() 166 | 167 | if args.debug: 168 | logging.basicConfig(level=logging.DEBUG) 169 | queries = args.queries 170 | if args.input: 171 | with open(args.input, "r") as f: 172 | for line in f: 173 | queries.append(line.strip("\n")) 174 | if not args.queries: 175 | raise ValueError("You must specify either at least one query or an input file") 176 | queries = list(dict.fromkeys(queries)) 177 | if args.platforms: 178 | platforms = [] 179 | for p in args.platforms: 180 | if p.upper() in Platforms.__members__: 181 | platforms.append(Platforms[p.upper()]) 182 | else: 183 | raise ValueError(p + " is not a valid platform") 184 | else: 185 | platforms = [p for p in Platforms] 186 | proxy_list = [] 187 | if args.proxy_list: 188 | with open(args.proxy_list, "r") as f: 189 | for line in f: 190 | proxy_list.append(line.strip("\n")) 191 | if args.view_key == "query": 192 | view_value = "platform" 193 | elif args.view_key == "platform": 194 | view_value = "query" 195 | 196 | async with aiohttp.ClientSession() as session: 197 | checkers = init_checkers(session, proxy_list=proxy_list) 198 | results = defaultdict(list) 199 | if args.cache_tokens: 200 | print("Caching tokens...", end="") 201 | await asyncio.gather(*(init_prerequest(platform, checkers) for platform in platforms)) 202 | print(end="\r") 203 | platform_queries = [query(q, p, checkers) for q in queries for p in platforms] 204 | for future in tqdm.tqdm( 205 | asyncio.as_completed(platform_queries), 206 | total=len(platform_queries), 207 | disable=args.verbose or args.debug, 208 | leave=False, 209 | ncols=BAR_WIDTH, 210 | bar_format=BAR_FORMAT, 211 | ): 212 | platform_response = await future 213 | if platform_response is None: 214 | continue 215 | if args.verbose: 216 | print(platform_response, getattr(platform_response, args.view_key)) 217 | print( 218 | f"Checked {platform_response.query: ^25} on {platform_response.platform.value.__name__:<10}: {platform_response.message}" 219 | ) 220 | results[str(getattr(platform_response, args.view_key))].append(platform_response) 221 | 222 | if args.json: 223 | print_json(results, file=args.json, available_only=args.available_only) 224 | else: 225 | pretty_print( 226 | results, 227 | view_value=view_value, 228 | available_only=args.available_only, 229 | show_urls=args.show_urls, 230 | ) 231 | print(f"Completed {len(platform_queries)} queries in {time.time() - start_time:.2f}s") 232 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /socialscan/platforms.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import abc 6 | import logging 7 | import re 8 | from dataclasses import dataclass 9 | from enum import Enum 10 | 11 | import aiohttp 12 | 13 | from socialscan import __version__ 14 | 15 | 16 | class QueryError(Exception): 17 | pass 18 | 19 | 20 | class UsernameQueryable(metaclass=abc.ABCMeta): 21 | """Abstract class for platforms that can query usernames.""" 22 | 23 | @abc.abstractmethod 24 | async def check_username(self, username): 25 | raise NotImplementedError 26 | 27 | 28 | class EmailQueryable(metaclass=abc.ABCMeta): 29 | """Abstract class for platforms that can query email addresses.""" 30 | 31 | @abc.abstractmethod 32 | async def check_email(self, email): 33 | raise NotImplementedError 34 | 35 | 36 | class PrerequestRequired(metaclass=abc.ABCMeta): 37 | """Abstract class for platforms that require a pre-request to retrieve a token, 38 | for use in the main query. This request is sent once and cached for future 39 | queries.""" 40 | 41 | @abc.abstractmethod 42 | async def prerequest(self): 43 | raise NotImplementedError 44 | 45 | async def get_token(self): 46 | """ 47 | Retrieve and return platform token using the `prerequest` method specified in the class 48 | 49 | Normal calls will not be able to take advantage of this as all tokens are retrieved concurrently 50 | This only applies to when tokens are retrieved before main queries with -c 51 | Adds 1-2s to overall running time but halves HTTP requests sent for bulk queries 52 | """ 53 | if self.prerequest_sent: 54 | if self.token is None: 55 | raise QueryError(BasePlatform.TOKEN_ERROR_MESSAGE) 56 | return self.token 57 | else: 58 | self.token = await self.prerequest() 59 | self.prerequest_sent = True 60 | if self.token is None: 61 | raise QueryError(BasePlatform.TOKEN_ERROR_MESSAGE) 62 | logging.debug(f"TOKEN {Platforms(self.__class__)}: {self.token}") 63 | return self.token 64 | 65 | 66 | class BasePlatform: 67 | # Default user agent taken from `odeialba/instagram-php-scraper` 68 | # https://github.com/odeialba/instagram-php-scraper/blob/39e8565e8446fa2c66dbcdee8807aa03fca2bbda/src/InstagramScraper/Instagram.php#L46 69 | DEFAULT_HEADERS = { 70 | "User-agent": "Mozilla/5.0 (Linux; Android 8.1.0; motorola one Build/OPKS28.63-18-3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/70.0.3538.80 Mobile Safari/537.36 Instagram 72.0.0.21.98 Android (27/8.1.0; 320dpi; 720x1362; motorola; motorola one; deen_sprout; qcom; pt_BR; 132081645)", 71 | "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", 72 | } 73 | UNEXPECTED_CONTENT_TYPE_ERROR_MESSAGE = "Unexpected content type {}. You might be sending too many requests. Use a proxy or wait before trying again." 74 | TOKEN_ERROR_MESSAGE = "Could not retrieve token. You might be sending too many requests. Use a proxy or wait before trying again." 75 | TOO_MANY_REQUEST_ERROR_MESSAGE = "Requests denied by platform due to excessive requests. Use a proxy or wait before trying again." 76 | TIMEOUT_DURATION = 15 77 | 78 | client_timeout = aiohttp.ClientTimeout(connect=TIMEOUT_DURATION) 79 | 80 | # 1: Be as explicit as possible in handling all cases 81 | # 2: Do not include any queries that will lead to side-effects on users (e.g. submitting sign up forms) 82 | # OK to omit checks for whether a key exists when parsing the JSON response. KeyError is handled by the parent coroutine. 83 | 84 | def response_failure(self, query, *, message="Failure"): 85 | return PlatformResponse( 86 | platform=Platforms(self.__class__), 87 | query=query, 88 | available=False, 89 | valid=False, 90 | success=False, 91 | message=message, 92 | link=None, 93 | ) 94 | 95 | def response_available(self, query, *, message="Available"): 96 | return PlatformResponse( 97 | platform=Platforms(self.__class__), 98 | query=query, 99 | available=True, 100 | valid=True, 101 | success=True, 102 | message=message, 103 | link=None, 104 | ) 105 | 106 | def response_unavailable(self, query, *, message="Unavailable", link=None): 107 | return PlatformResponse( 108 | platform=Platforms(self.__class__), 109 | query=query, 110 | available=False, 111 | valid=True, 112 | success=True, 113 | message=message, 114 | link=link, 115 | ) 116 | 117 | def response_invalid(self, query, *, message="Invalid"): 118 | return PlatformResponse( 119 | platform=Platforms(self.__class__), 120 | query=query, 121 | available=False, 122 | valid=False, 123 | success=True, 124 | message=message, 125 | link=None, 126 | ) 127 | 128 | def response_unavailable_or_invalid(self, query, *, message, unavailable_messages, link=None): 129 | if any(x in message for x in unavailable_messages): 130 | return self.response_unavailable(query, message=message, link=link) 131 | else: 132 | return self.response_invalid(query, message=message) 133 | 134 | def _request(self, method, url, **kwargs): 135 | proxy = ( 136 | self.proxy_list[self.request_count % len(self.proxy_list)] if self.proxy_list else None 137 | ) 138 | self.request_count += 1 139 | if "headers" in kwargs: 140 | kwargs["headers"].update(BasePlatform.DEFAULT_HEADERS) 141 | else: 142 | kwargs["headers"] = BasePlatform.DEFAULT_HEADERS 143 | return self.session.request(method, url, timeout=self.client_timeout, proxy=proxy, **kwargs) 144 | 145 | def post(self, url, **kwargs): 146 | logging.debug(f"POST {url}") 147 | return self._request("POST", url, **kwargs) 148 | 149 | def get(self, url, **kwargs): 150 | logging.debug(f"GET {url}") 151 | return self._request("GET", url, **kwargs) 152 | 153 | @staticmethod 154 | async def get_json(request): 155 | if not request.headers["Content-Type"].startswith("application/json"): 156 | raise QueryError( 157 | BasePlatform.UNEXPECTED_CONTENT_TYPE_ERROR_MESSAGE.format( 158 | request.headers["Content-Type"] 159 | ) 160 | ) 161 | else: 162 | json = await request.json() 163 | logging.debug(f"JSON {request.url} {request.status}: {json}") 164 | return json 165 | 166 | @staticmethod 167 | async def get_text(request): 168 | text = await request.text() 169 | logging.debug(f"TEXT {request.url} {request.status}: {text}") 170 | return text 171 | 172 | def __init__(self, session, proxy_list=[]): 173 | self.session = session 174 | self.proxy_list = proxy_list 175 | self.request_count = 0 176 | self.prerequest_sent = False 177 | self.token = None 178 | 179 | 180 | class Snapchat(BasePlatform, UsernameQueryable, PrerequestRequired): 181 | URL = "https://accounts.snapchat.com/accounts/login" 182 | ENDPOINT = "https://accounts.snapchat.com/accounts/get_username_suggestions" 183 | USERNAME_TAKEN_MSGS = ["is already taken", "is currently unavailable"] 184 | 185 | async def prerequest(self): 186 | async with self.get(Snapchat.URL) as r: 187 | """ 188 | See: https://github.com/aio-libs/aiohttp/issues/3002 189 | Snapchat sends multiple Set-Cookie headers in its response setting the value of 'xsrf-token', 190 | causing the original value of 'xsrf-token' to be overwritten in aiohttp 191 | Need to analyse the header response to extract the required value 192 | """ 193 | cookies = r.headers.getall("Set-Cookie") 194 | for cookie in cookies: 195 | match = re.search(r"xsrf_token=([\w-]*);", cookie) 196 | if match: 197 | token = match.group(1) 198 | return token 199 | 200 | async def check_username(self, username): 201 | token = await self.get_token() 202 | async with self.post( 203 | Snapchat.ENDPOINT, 204 | data={"requested_username": username, "xsrf_token": token}, 205 | cookies={"xsrf_token": token}, 206 | ) as r: 207 | # Non-JSON received if too many requests 208 | json_body = await self.get_json(r) 209 | if "error_message" in json_body["value"]: 210 | return self.response_unavailable_or_invalid( 211 | username, 212 | message=json_body["value"]["error_message"], 213 | unavailable_messages=Snapchat.USERNAME_TAKEN_MSGS, 214 | ) 215 | elif json_body["value"]["status_code"] == "OK": 216 | return self.response_available(username) 217 | 218 | # Email: Snapchat doesn't associate email addresses with accounts 219 | 220 | 221 | class Instagram(BasePlatform, UsernameQueryable, EmailQueryable, PrerequestRequired): 222 | URL = "https://www.instagram.com/api/v1/public/landing_info/" 223 | ENDPOINT = "https://www.instagram.com/accounts/web_create_ajax/attempt/" 224 | USERNAME_TAKEN_MSGS = [ 225 | "This username isn't available.", 226 | "A user with that username already exists.", 227 | ] 228 | USERNAME_LINK_FORMAT = "https://www.instagram.com/{}" 229 | 230 | async def prerequest(self): 231 | async with self.get(Instagram.URL) as r: 232 | if "csrftoken" in r.cookies: 233 | token = r.cookies["csrftoken"].value 234 | return token 235 | 236 | async def check_username(self, username): 237 | token = await self.get_token() 238 | async with self.post( 239 | Instagram.ENDPOINT, data={"username": username}, headers={"x-csrftoken": token} 240 | ) as r: 241 | json_body = await self.get_json(r) 242 | # Too many requests 243 | if json_body["status"] == "fail": 244 | return self.response_failure(username, message=json_body["message"]) 245 | if "username" in json_body["errors"]: 246 | return self.response_unavailable_or_invalid( 247 | username, 248 | message=json_body["errors"]["username"][0]["message"], 249 | unavailable_messages=Instagram.USERNAME_TAKEN_MSGS, 250 | link=Instagram.USERNAME_LINK_FORMAT.format(username), 251 | ) 252 | else: 253 | return self.response_available(username) 254 | 255 | async def check_email(self, email): 256 | token = await self.get_token() 257 | async with self.post( 258 | Instagram.ENDPOINT, data={"email": email}, headers={"x-csrftoken": token} 259 | ) as r: 260 | json_body = await self.get_json(r) 261 | # Too many requests 262 | if json_body["status"] == "fail": 263 | return self.response_failure(email, message=json_body["message"]) 264 | if "email" not in json_body["errors"]: 265 | return self.response_available(email) 266 | else: 267 | message = json_body["errors"]["email"][0]["message"] 268 | if json_body["errors"]["email"][0]["code"] == "invalid_email": 269 | return self.response_invalid(email, message=message) 270 | else: 271 | return self.response_unavailable(email, message=message) 272 | 273 | 274 | class GitHub(BasePlatform, UsernameQueryable, EmailQueryable, PrerequestRequired): 275 | URL = "https://github.com/join" 276 | USERNAME_ENDPOINT = "https://github.com/signup_check/username" 277 | EMAIL_ENDPOINT = "https://github.com/signup_check/email" 278 | # [username taken, reserved keyword (Username __ is unavailable)] 279 | USERNAME_TAKEN_MSGS = ["already taken", "unavailable", "not available"] 280 | USERNAME_LINK_FORMAT = "https://github.com/{}" 281 | 282 | token_regex = re.compile( 283 | r']+>") 286 | 287 | async def prerequest(self): 288 | async with self.get(GitHub.URL) as r: 289 | text = await self.get_text(r) 290 | match = self.token_regex.search(text) 291 | if match: 292 | username_token = match.group(1) 293 | email_token = match.group(2) 294 | return (username_token, email_token) 295 | 296 | async def check_username(self, username): 297 | pr = await self.get_token() 298 | (username_token, _) = pr 299 | async with self.post( 300 | GitHub.USERNAME_ENDPOINT, 301 | data={"value": username, "authenticity_token": username_token}, 302 | ) as r: 303 | if r.status == 422: 304 | text = await self.get_text(r) 305 | text = self.tag_regex.sub("", text).strip() 306 | return self.response_unavailable_or_invalid( 307 | username, 308 | message=text, 309 | unavailable_messages=GitHub.USERNAME_TAKEN_MSGS, 310 | link=GitHub.USERNAME_LINK_FORMAT.format(username), 311 | ) 312 | elif r.status == 200: 313 | return self.response_available(username) 314 | elif r.status == 429: 315 | return self.response_failure( 316 | username, message=BasePlatform.TOO_MANY_REQUEST_ERROR_MESSAGE 317 | ) 318 | 319 | async def check_email(self, email): 320 | pr = await self.get_token() 321 | if pr is None: 322 | return self.response_failure(email, message=BasePlatform.TOKEN_ERROR_MESSAGE) 323 | else: 324 | (_, email_token) = pr 325 | async with self.post( 326 | GitHub.EMAIL_ENDPOINT, 327 | data={"value": email, "authenticity_token": email_token}, 328 | ) as r: 329 | if r.status == 422: 330 | text = await self.get_text(r) 331 | return self.response_unavailable(email, message=text) 332 | elif r.status == 200: 333 | return self.response_available(email) 334 | elif r.status == 429: 335 | return self.response_failure( 336 | email, message=BasePlatform.TOO_MANY_REQUEST_ERROR_MESSAGE 337 | ) 338 | 339 | 340 | class Tumblr(BasePlatform, UsernameQueryable, EmailQueryable, PrerequestRequired): 341 | URL = "https://tumblr.com/register" 342 | ENDPOINT = "https://www.tumblr.com/api/v2/register/account/validate" 343 | USERNAME_LINK_FORMAT = "https://{}.tumblr.com" 344 | 345 | SAMPLE_UNUSED_EMAIL = "akc2rW33AuSqQWY8@gmail.com" 346 | SAMPLE_PASSWORD = "correcthorsebatterystaple" 347 | SAMPLE_UNUSED_USERNAME = "akc2rW33AuSqQWY8" 348 | 349 | async def prerequest(self): 350 | async with self.get(Tumblr.URL) as r: 351 | text = await self.get_text(r) 352 | match = re.search(r'"API_TOKEN":"([\s\S]+?)"', text) 353 | if match: 354 | token = match.group(1) 355 | return token 356 | 357 | async def _check(self, email=SAMPLE_UNUSED_EMAIL, username=SAMPLE_UNUSED_USERNAME): 358 | query = email if username == Tumblr.SAMPLE_UNUSED_USERNAME else username 359 | token = await self.get_token() 360 | async with self.post( 361 | Tumblr.ENDPOINT, 362 | json={ 363 | "email": email, 364 | "tumblelog": username, 365 | "password": Tumblr.SAMPLE_PASSWORD, 366 | }, 367 | headers={ 368 | "authorization": f"Bearer {token}", 369 | }, 370 | ) as r: 371 | json_body = await self.get_json(r) 372 | if "error" in json_body["response"] and "code" in json_body["response"]: 373 | if json_body["response"]["code"] == 3 and username == query: 374 | return self.response_unavailable( 375 | username, 376 | message=json_body["response"]["error"], 377 | link=Tumblr.USERNAME_LINK_FORMAT.format(query), 378 | ) 379 | elif json_body["response"]["code"] == 2 and email == query: 380 | return self.response_unavailable( 381 | email, 382 | message=json_body["response"]["error"], 383 | link=Tumblr.USERNAME_LINK_FORMAT.format(query), 384 | ) 385 | else: 386 | return self.response_invalid(query, message=json_body["response"]["error"]) 387 | elif json_body["meta"]["status"] == 200: 388 | return self.response_available(query) 389 | else: 390 | return self.response_failure(query, message="Unknown response") 391 | 392 | async def check_username(self, username): 393 | return await self._check(username=username) 394 | 395 | async def check_email(self, email): 396 | return await self._check(email=email) 397 | 398 | 399 | class GitLab(BasePlatform, UsernameQueryable): 400 | URL = "https://gitlab.com/users/sign_in" 401 | ENDPOINT = "https://gitlab.com/users/{}/exists" 402 | USERNAME_LINK_FORMAT = "https://gitlab.com/{}" 403 | 404 | async def check_username(self, username): 405 | # Custom matching required as validation is implemented locally and not server-side by GitLab 406 | if not re.fullmatch( 407 | r"[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_]", username 408 | ): 409 | return self.response_invalid( 410 | username, message="Please create a username with only alphanumeric characters." 411 | ) 412 | async with self.get( 413 | GitLab.ENDPOINT.format(username), headers={"X-Requested-With": "XMLHttpRequest"} 414 | ) as r: 415 | # Special case for usernames 416 | if r.status == 401: 417 | return self.response_unavailable( 418 | username, link=GitLab.USERNAME_LINK_FORMAT.format(username) 419 | ) 420 | json_body = await self.get_json(r) 421 | if json_body["exists"]: 422 | return self.response_unavailable( 423 | username, link=GitLab.USERNAME_LINK_FORMAT.format(username) 424 | ) 425 | else: 426 | return self.response_available(username) 427 | 428 | # Email: GitLab requires a reCAPTCHA token to check email address usage which we cannot bypass 429 | 430 | 431 | class Reddit(BasePlatform, UsernameQueryable): 432 | URL = "https://reddit.com" 433 | ENDPOINT = "https://www.reddit.com/api/check_username.json" 434 | USERNAME_TAKEN_MSGS = [ 435 | "that username is already taken", 436 | "that username is taken by a deleted account", 437 | ] 438 | USERNAME_LINK_FORMAT = "https://www.reddit.com/u/{}" 439 | 440 | async def check_username(self, username): 441 | # Custom user agent required to overcome rate limits for Reddit API 442 | async with self.post(Reddit.ENDPOINT, data={"user": username}) as r: 443 | json_body = await self.get_json(r) 444 | if "error" in json_body and json_body["error"] == 429: 445 | return self.response_failure( 446 | username, message=BasePlatform.TOO_MANY_REQUEST_ERROR_MESSAGE 447 | ) 448 | elif "json" in json_body: 449 | return self.response_unavailable_or_invalid( 450 | username, 451 | message=json_body["json"]["errors"][0][1], 452 | unavailable_messages=Reddit.USERNAME_TAKEN_MSGS, 453 | link=Reddit.USERNAME_LINK_FORMAT.format(username), 454 | ) 455 | elif json_body == {}: 456 | return self.response_available(username) 457 | 458 | # Email: You can register multiple Reddit accounts under the same email address so not possible to check if an address is in use 459 | 460 | 461 | class Twitter(BasePlatform, UsernameQueryable, EmailQueryable): 462 | URL = "https://twitter.com/signup" 463 | USERNAME_ENDPOINT = "https://api.twitter.com/i/users/username_available.json" 464 | EMAIL_ENDPOINT = "https://api.twitter.com/i/users/email_available.json" 465 | # [account in use, account suspended] 466 | USERNAME_TAKEN_MSGS = ["That username has been taken", "unavailable"] 467 | USERNAME_LINK_FORMAT = "https://twitter.com/{}" 468 | 469 | async def check_username(self, username): 470 | async with self.get(Twitter.USERNAME_ENDPOINT, params={"username": username}) as r: 471 | json_body = await self.get_json(r) 472 | message = json_body["desc"] 473 | if json_body["valid"]: 474 | return self.response_available(username, message=message) 475 | else: 476 | return self.response_unavailable_or_invalid( 477 | username, 478 | message=message, 479 | unavailable_messages=Twitter.USERNAME_TAKEN_MSGS, 480 | link=Twitter.USERNAME_LINK_FORMAT.format(username), 481 | ) 482 | 483 | async def check_email(self, email): 484 | async with self.get(Twitter.EMAIL_ENDPOINT, params={"email": email}) as r: 485 | json_body = await self.get_json(r) 486 | message = json_body["msg"] 487 | if not json_body["valid"] and not json_body["taken"]: 488 | return self.response_invalid(email, message=message) 489 | 490 | if json_body["taken"]: 491 | return self.response_unavailable(email, message=message) 492 | else: 493 | return self.response_available(email, message=message) 494 | 495 | 496 | class Pinterest(BasePlatform, EmailQueryable): 497 | URL = "https://www.pinterest.com" 498 | EMAIL_ENDPOINT = "https://www.pinterest.com/_ngjs/resource/EmailExistsResource/get/" 499 | 500 | async def check_email(self, email): 501 | data = '{"options": {"email": "%s"}, "context": {}}' % email 502 | async with self.get( 503 | Pinterest.EMAIL_ENDPOINT, params={"source_url": "/", "data": data} 504 | ) as r: 505 | json_body = await self.get_json(r) 506 | email_exists = json_body["resource_response"]["data"] 507 | if email_exists: 508 | return self.response_unavailable(email) 509 | else: 510 | return self.response_available(email) 511 | 512 | 513 | class Lastfm(BasePlatform, UsernameQueryable, EmailQueryable, PrerequestRequired): 514 | URL = "https://www.last.fm/join" 515 | ENDPOINT = "https://www.last.fm/join/partial/validate" 516 | USERNAME_TAKEN_MSGS = ["Sorry, this username isn't available."] 517 | USERNAME_LINK_FORMAT = "https://www.last.fm/user/{}" 518 | 519 | async def prerequest(self): 520 | async with self.get(Lastfm.URL) as r: 521 | if "csrftoken" in r.cookies: 522 | token = r.cookies["csrftoken"].value 523 | return token 524 | 525 | async def _check(self, username="", email=""): 526 | token = await self.get_token() 527 | data = {"csrfmiddlewaretoken": token, "userName": username, "email": email} 528 | headers = { 529 | "Accept": "*/*", 530 | "Referer": "https://www.last.fm/join", 531 | "X-Requested-With": "XMLHttpRequest", 532 | "Cookie": f"csrftoken={token}", 533 | } 534 | async with self.post(Lastfm.ENDPOINT, data=data, headers=headers) as r: 535 | json_body = await self.get_json(r) 536 | if email: 537 | if json_body["email"]["valid"]: 538 | return self.response_available( 539 | email, message=json_body["email"]["success_message"] 540 | ) 541 | else: 542 | return self.response_unavailable( 543 | email, message=json_body["email"]["error_messages"][0] 544 | ) 545 | elif username: 546 | if json_body["userName"]["valid"]: 547 | return self.response_available( 548 | username, message=json_body["userName"]["success_message"] 549 | ) 550 | else: 551 | return self.response_unavailable_or_invalid( 552 | username, 553 | message=re.sub("<[^<]+?>", "", json_body["userName"]["error_messages"][0]), 554 | unavailable_messages=Lastfm.USERNAME_TAKEN_MSGS, 555 | link=Lastfm.USERNAME_LINK_FORMAT.format(username), 556 | ) 557 | 558 | async def check_email(self, email): 559 | return await self._check(email=email) 560 | 561 | async def check_username(self, username): 562 | return await self._check(username=username) 563 | 564 | 565 | class Yahoo(BasePlatform, UsernameQueryable, PrerequestRequired): 566 | URL = "https://login.yahoo.com/account/create" 567 | USERNAME_ENDPOINT = "https://login.yahoo.com/account/module/create?validateField=yid" 568 | 569 | # Modified from Yahoo source 570 | error_messages = { 571 | "IDENTIFIER_EXISTS": "A Yahoo account already exists with this email address. REPLACE_SIGNIN_LINK.", 572 | "DANGLING_IDENTIFIER_EXISTS": "A Yahoo account already exists with this email address.", 573 | "IDENTIFIER_NOT_AVAILABLE": "This email address is not available for sign up, try something else", 574 | "EMAIL_DOMAIN_NOT_ALLOWED": "You cannot use this email address. Instead try creating Yahoo email address", 575 | "RESERVED_WORD_PRESENT": "A Yahoo account already exists with this email address.", 576 | "SOME_SPECIAL_CHARACTERS_NOT_ALLOWED": "You can only use letters, numbers, periods (‘.’), and underscores (‘_’) in your username.", 577 | "SOME_SPECIAL_CHARACTERS_NOT_ALLOWED_IN_EMAIL": "Make sure you use your full email address, including an “@” sign and a domain.", 578 | "INVALID_IDENTIFIER": "Error: Invalid identifier.", 579 | "CANNOT_END_WITH_SPECIAL_CHARACTER": "Your username has to end with a letter or a number.", 580 | "CANNOT_HAVE_MORE_THAN_ONE_PERIOD": "You can’t have more than one ‘.’ in your username.", 581 | "NEED_AT_LEAST_ONE_ALPHA": "Please use at least one letter in your username.", 582 | "CANNOT_START_WITH_SPECIAL_CHARACTER_OR_NUMBER": "Your username has to start with a letter.", 583 | "CONSECUTIVE_SPECIAL_CHARACTERS_NOT_ALLOWED": "You can’t have more than one ‘.’ or ‘_’ in a row.", 584 | "INVALID_NAME_LENGTH": "That name is too long.", 585 | "LENGTH_TOO_SHORT": "That email address is too short, please use a longer one.", 586 | "LENGTH_TOO_LONG": "That email address is too long, please use a shorter one.", 587 | "NAME_CONTAINS_URL": "You can't use this name", 588 | "ELECTION_SPECIFIC_WORD_PRESENT": "Not available, try something else.", 589 | } 590 | 591 | regex = re.compile(r"v=1&s=([^\s]*)") 592 | 593 | async def prerequest(self): 594 | async with self.get(Yahoo.URL) as r: 595 | if "AS" in r.cookies: 596 | match = self.regex.search(r.cookies["AS"].value) 597 | if match: 598 | return match.group(1) 599 | 600 | async def check_username(self, username): 601 | token = await self.get_token() 602 | async with self.post( 603 | Yahoo.USERNAME_ENDPOINT, 604 | data={"specId": "yidReg", "acrumb": token, "yid": username}, 605 | headers={"X-Requested-With": "XMLHttpRequest"}, 606 | ) as r: 607 | json_body = await self.get_json(r) 608 | if json_body["errors"][2]["name"] != "yid": 609 | return self.response_available(username) 610 | else: 611 | error = json_body["errors"][2]["error"] 612 | error_pretty = self.error_messages.get(error, error.replace("_", " ").capitalize()) 613 | if error in ( 614 | "IDENTIFIER_EXISTS", 615 | "RESERVED_WORD_PRESENT", 616 | "IDENTIFIER_NOT_AVAILABLE", 617 | "DANGLING_IDENTIFIER_EXISTS", 618 | ): 619 | return self.response_unavailable(username, message=error_pretty) 620 | else: 621 | return self.response_invalid(username, message=error_pretty) 622 | 623 | 624 | class Firefox(BasePlatform, EmailQueryable): 625 | URL = "https://accounts.firefox.com/signup" 626 | EMAIL_ENDPOINT = "https://api.accounts.firefox.com/v1/account/status" 627 | 628 | async def check_email(self, email): 629 | async with self.post(Firefox.EMAIL_ENDPOINT, data={"email": email}) as r: 630 | json_body = await self.get_json(r) 631 | if "error" in json_body: 632 | return self.response_failure(email, message=json_body["message"]) 633 | elif json_body["exists"]: 634 | return self.response_unavailable(email) 635 | else: 636 | return self.response_available(email) 637 | 638 | 639 | class Platforms(Enum): 640 | GITHUB = GitHub 641 | GITLAB = GitLab 642 | INSTAGRAM = Instagram 643 | PINTEREST = Pinterest 644 | REDDIT = Reddit 645 | TWITTER = Twitter 646 | TUMBLR = Tumblr 647 | FIREFOX = Firefox 648 | 649 | def __str__(self): 650 | return self.value.__name__ 651 | 652 | def __len__(self): 653 | return len(self.value.__name__) 654 | 655 | 656 | @dataclass(frozen=True) 657 | class PlatformResponse: 658 | platform: Platforms 659 | query: str 660 | available: bool 661 | valid: bool 662 | success: bool 663 | message: str 664 | link: str 665 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "c9c4f8e98f062873d06d00656171fc6a95b5109097a4cb50e9179cd705693f78" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aiohttp": { 20 | "hashes": [ 21 | "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f", 22 | "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c", 23 | "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af", 24 | "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4", 25 | "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a", 26 | "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489", 27 | "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213", 28 | "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01", 29 | "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5", 30 | "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361", 31 | "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26", 32 | "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0", 33 | "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4", 34 | "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8", 35 | "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1", 36 | "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7", 37 | "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6", 38 | "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a", 39 | "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd", 40 | "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4", 41 | "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499", 42 | "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183", 43 | "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544", 44 | "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821", 45 | "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501", 46 | "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f", 47 | "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe", 48 | "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f", 49 | "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672", 50 | "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5", 51 | "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2", 52 | "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57", 53 | "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87", 54 | "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0", 55 | "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f", 56 | "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7", 57 | "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed", 58 | "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70", 59 | "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0", 60 | "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f", 61 | "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d", 62 | "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f", 63 | "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d", 64 | "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431", 65 | "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff", 66 | "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf", 67 | "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83", 68 | "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690", 69 | "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587", 70 | "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e", 71 | "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb", 72 | "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3", 73 | "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66", 74 | "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014", 75 | "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35", 76 | "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f", 77 | "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0", 78 | "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449", 79 | "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23", 80 | "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5", 81 | "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd", 82 | "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4", 83 | "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b", 84 | "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558", 85 | "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd", 86 | "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766", 87 | "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a", 88 | "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636", 89 | "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d", 90 | "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590", 91 | "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e", 92 | "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d", 93 | "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c", 94 | "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28", 95 | "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065", 96 | "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca" 97 | ], 98 | "markers": "python_version >= '3.8'", 99 | "version": "==3.9.1" 100 | }, 101 | "aiosignal": { 102 | "hashes": [ 103 | "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", 104 | "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" 105 | ], 106 | "markers": "python_version >= '3.7'", 107 | "version": "==1.3.1" 108 | }, 109 | "async-timeout": { 110 | "hashes": [ 111 | "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", 112 | "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" 113 | ], 114 | "markers": "python_version < '3.11'", 115 | "version": "==4.0.3" 116 | }, 117 | "attrs": { 118 | "hashes": [ 119 | "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", 120 | "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" 121 | ], 122 | "markers": "python_version >= '3.7'", 123 | "version": "==23.1.0" 124 | }, 125 | "colorama": { 126 | "hashes": [ 127 | "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", 128 | "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" 129 | ], 130 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 131 | "version": "==0.4.6" 132 | }, 133 | "frozenlist": { 134 | "hashes": [ 135 | "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", 136 | "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", 137 | "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", 138 | "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", 139 | "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", 140 | "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", 141 | "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", 142 | "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", 143 | "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", 144 | "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", 145 | "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", 146 | "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", 147 | "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", 148 | "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", 149 | "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", 150 | "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", 151 | "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", 152 | "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", 153 | "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", 154 | "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", 155 | "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", 156 | "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", 157 | "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", 158 | "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", 159 | "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", 160 | "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", 161 | "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", 162 | "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", 163 | "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", 164 | "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", 165 | "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", 166 | "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", 167 | "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", 168 | "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", 169 | "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", 170 | "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", 171 | "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", 172 | "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", 173 | "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", 174 | "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", 175 | "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", 176 | "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", 177 | "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", 178 | "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", 179 | "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", 180 | "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", 181 | "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", 182 | "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", 183 | "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", 184 | "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", 185 | "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", 186 | "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", 187 | "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", 188 | "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", 189 | "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", 190 | "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", 191 | "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", 192 | "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", 193 | "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", 194 | "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", 195 | "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", 196 | "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", 197 | "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", 198 | "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", 199 | "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", 200 | "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", 201 | "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", 202 | "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", 203 | "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", 204 | "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", 205 | "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", 206 | "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", 207 | "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", 208 | "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", 209 | "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", 210 | "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", 211 | "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" 212 | ], 213 | "markers": "python_version >= '3.8'", 214 | "version": "==1.4.1" 215 | }, 216 | "idna": { 217 | "hashes": [ 218 | "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", 219 | "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" 220 | ], 221 | "markers": "python_version >= '3.5'", 222 | "version": "==3.6" 223 | }, 224 | "multidict": { 225 | "hashes": [ 226 | "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9", 227 | "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8", 228 | "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03", 229 | "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710", 230 | "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161", 231 | "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664", 232 | "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569", 233 | "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067", 234 | "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313", 235 | "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706", 236 | "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2", 237 | "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636", 238 | "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49", 239 | "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93", 240 | "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603", 241 | "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0", 242 | "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60", 243 | "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4", 244 | "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e", 245 | "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1", 246 | "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60", 247 | "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951", 248 | "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc", 249 | "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe", 250 | "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95", 251 | "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d", 252 | "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8", 253 | "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed", 254 | "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2", 255 | "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775", 256 | "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87", 257 | "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c", 258 | "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2", 259 | "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98", 260 | "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3", 261 | "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe", 262 | "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78", 263 | "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660", 264 | "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176", 265 | "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e", 266 | "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988", 267 | "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c", 268 | "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c", 269 | "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0", 270 | "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449", 271 | "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f", 272 | "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde", 273 | "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5", 274 | "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d", 275 | "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac", 276 | "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a", 277 | "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9", 278 | "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca", 279 | "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11", 280 | "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35", 281 | "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063", 282 | "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b", 283 | "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982", 284 | "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258", 285 | "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1", 286 | "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52", 287 | "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480", 288 | "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7", 289 | "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461", 290 | "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d", 291 | "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc", 292 | "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779", 293 | "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a", 294 | "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547", 295 | "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0", 296 | "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171", 297 | "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf", 298 | "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d", 299 | "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba" 300 | ], 301 | "markers": "python_version >= '3.7'", 302 | "version": "==6.0.4" 303 | }, 304 | "socialscan": { 305 | "editable": true, 306 | "markers": "python_version >= '3.6'", 307 | "path": "." 308 | }, 309 | "tqdm": { 310 | "hashes": [ 311 | "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386", 312 | "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7" 313 | ], 314 | "markers": "python_version >= '3.7'", 315 | "version": "==4.66.1" 316 | }, 317 | "yarl": { 318 | "hashes": [ 319 | "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", 320 | "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", 321 | "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", 322 | "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", 323 | "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", 324 | "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", 325 | "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", 326 | "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", 327 | "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", 328 | "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", 329 | "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", 330 | "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", 331 | "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", 332 | "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", 333 | "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", 334 | "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", 335 | "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", 336 | "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", 337 | "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", 338 | "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", 339 | "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", 340 | "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", 341 | "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", 342 | "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", 343 | "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", 344 | "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", 345 | "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", 346 | "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", 347 | "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", 348 | "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", 349 | "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", 350 | "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", 351 | "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", 352 | "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", 353 | "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", 354 | "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", 355 | "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", 356 | "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", 357 | "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", 358 | "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", 359 | "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", 360 | "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", 361 | "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", 362 | "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", 363 | "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", 364 | "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", 365 | "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", 366 | "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", 367 | "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", 368 | "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", 369 | "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", 370 | "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", 371 | "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", 372 | "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", 373 | "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", 374 | "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", 375 | "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", 376 | "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", 377 | "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", 378 | "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", 379 | "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", 380 | "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", 381 | "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", 382 | "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", 383 | "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", 384 | "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", 385 | "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", 386 | "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", 387 | "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", 388 | "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", 389 | "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", 390 | "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", 391 | "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", 392 | "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", 393 | "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", 394 | "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", 395 | "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", 396 | "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", 397 | "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", 398 | "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", 399 | "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", 400 | "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", 401 | "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", 402 | "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", 403 | "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", 404 | "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", 405 | "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", 406 | "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", 407 | "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", 408 | "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" 409 | ], 410 | "markers": "python_version >= '3.7'", 411 | "version": "==1.9.4" 412 | } 413 | }, 414 | "develop": { 415 | "black": { 416 | "hashes": [ 417 | "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", 418 | "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f", 419 | "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e", 420 | "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec", 421 | "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055", 422 | "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3", 423 | "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5", 424 | "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54", 425 | "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b", 426 | "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e", 427 | "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e", 428 | "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba", 429 | "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea", 430 | "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59", 431 | "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d", 432 | "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0", 433 | "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9", 434 | "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a", 435 | "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e", 436 | "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba", 437 | "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2", 438 | "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2" 439 | ], 440 | "index": "pypi", 441 | "markers": "python_version >= '3.8'", 442 | "version": "==23.12.1" 443 | }, 444 | "cachetools": { 445 | "hashes": [ 446 | "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2", 447 | "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1" 448 | ], 449 | "markers": "python_version >= '3.7'", 450 | "version": "==5.3.2" 451 | }, 452 | "chardet": { 453 | "hashes": [ 454 | "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", 455 | "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970" 456 | ], 457 | "markers": "python_version >= '3.7'", 458 | "version": "==5.2.0" 459 | }, 460 | "click": { 461 | "hashes": [ 462 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 463 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 464 | ], 465 | "markers": "python_version >= '3.7'", 466 | "version": "==8.1.7" 467 | }, 468 | "colorama": { 469 | "hashes": [ 470 | "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", 471 | "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" 472 | ], 473 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 474 | "version": "==0.4.6" 475 | }, 476 | "distlib": { 477 | "hashes": [ 478 | "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", 479 | "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64" 480 | ], 481 | "version": "==0.3.8" 482 | }, 483 | "exceptiongroup": { 484 | "hashes": [ 485 | "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", 486 | "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" 487 | ], 488 | "markers": "python_version < '3.11'", 489 | "version": "==1.2.0" 490 | }, 491 | "filelock": { 492 | "hashes": [ 493 | "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e", 494 | "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c" 495 | ], 496 | "markers": "python_version >= '3.8'", 497 | "version": "==3.13.1" 498 | }, 499 | "flake8": { 500 | "hashes": [ 501 | "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", 502 | "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" 503 | ], 504 | "index": "pypi", 505 | "markers": "python_full_version >= '3.8.1'", 506 | "version": "==6.1.0" 507 | }, 508 | "iniconfig": { 509 | "hashes": [ 510 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 511 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 512 | ], 513 | "markers": "python_version >= '3.7'", 514 | "version": "==2.0.0" 515 | }, 516 | "mccabe": { 517 | "hashes": [ 518 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 519 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 520 | ], 521 | "markers": "python_version >= '3.6'", 522 | "version": "==0.7.0" 523 | }, 524 | "mypy-extensions": { 525 | "hashes": [ 526 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 527 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 528 | ], 529 | "markers": "python_version >= '3.5'", 530 | "version": "==1.0.0" 531 | }, 532 | "packaging": { 533 | "hashes": [ 534 | "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", 535 | "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" 536 | ], 537 | "markers": "python_version >= '3.7'", 538 | "version": "==23.2" 539 | }, 540 | "pathspec": { 541 | "hashes": [ 542 | "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", 543 | "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" 544 | ], 545 | "markers": "python_version >= '3.8'", 546 | "version": "==0.12.1" 547 | }, 548 | "platformdirs": { 549 | "hashes": [ 550 | "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", 551 | "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" 552 | ], 553 | "markers": "python_version >= '3.8'", 554 | "version": "==4.1.0" 555 | }, 556 | "pluggy": { 557 | "hashes": [ 558 | "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", 559 | "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" 560 | ], 561 | "markers": "python_version >= '3.8'", 562 | "version": "==1.3.0" 563 | }, 564 | "pycodestyle": { 565 | "hashes": [ 566 | "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", 567 | "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" 568 | ], 569 | "markers": "python_version >= '3.8'", 570 | "version": "==2.11.1" 571 | }, 572 | "pyflakes": { 573 | "hashes": [ 574 | "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", 575 | "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" 576 | ], 577 | "markers": "python_version >= '3.8'", 578 | "version": "==3.1.0" 579 | }, 580 | "pyproject-api": { 581 | "hashes": [ 582 | "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538", 583 | "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675" 584 | ], 585 | "markers": "python_version >= '3.8'", 586 | "version": "==1.6.1" 587 | }, 588 | "pytest": { 589 | "hashes": [ 590 | "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", 591 | "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" 592 | ], 593 | "index": "pypi", 594 | "markers": "python_version >= '3.7'", 595 | "version": "==7.4.3" 596 | }, 597 | "tomli": { 598 | "hashes": [ 599 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 600 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 601 | ], 602 | "markers": "python_version < '3.11'", 603 | "version": "==2.0.1" 604 | }, 605 | "tox": { 606 | "hashes": [ 607 | "sha256:2adb83d68f27116812b69aa36676a8d6a52249cb0d173649de0e7d0c2e3e7229", 608 | "sha256:73a7240778fabf305aeb05ab8ea26e575e042ab5a18d71d0ed13e343a51d6ce1" 609 | ], 610 | "index": "pypi", 611 | "markers": "python_version >= '3.8'", 612 | "version": "==4.11.4" 613 | }, 614 | "typing-extensions": { 615 | "hashes": [ 616 | "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", 617 | "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" 618 | ], 619 | "markers": "python_version < '3.11'", 620 | "version": "==4.9.0" 621 | }, 622 | "virtualenv": { 623 | "hashes": [ 624 | "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3", 625 | "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b" 626 | ], 627 | "markers": "python_version >= '3.7'", 628 | "version": "==20.25.0" 629 | } 630 | } 631 | } 632 | --------------------------------------------------------------------------------