├── source ├── .env ├── __init__.py ├── helper │ ├── __init__.py │ └── complement.py ├── interface │ ├── __init__.py │ ├── extension │ │ ├── __init__.py │ │ ├── extension_manager.py │ │ └── plugin_manager.py │ ├── interface.py │ └── driver_interface.py ├── service │ ├── __init__.py │ ├── extension │ │ ├── __init__.py │ │ ├── download_extension_firefox.py │ │ └── download_extension_chrome.py │ ├── user_agent_browser.py │ ├── driver_manager.py │ ├── driver │ │ ├── configuration_firefox.py │ │ └── configuration_chrome.py │ └── driver_action.py ├── controller │ ├── __init__.py │ ├── extension │ │ ├── __init__.py │ │ ├── captchasolver.py │ │ └── metamask.py │ ├── runner.py │ ├── extension_manager.py │ └── web_driver.py ├── .pylintrc ├── .idea │ ├── vcs.xml │ ├── misc.xml │ ├── .gitignore │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── modules.xml │ └── source.iml ├── main.py └── requirements.txt ├── .pylintrc ├── .idea ├── vcs.xml ├── .gitignore ├── misc.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── modules.xml └── selenium-docker.iml ├── docker ├── python │ └── Dockerfile └── single │ └── Dockerfile ├── .env.example ├── .github └── workflows │ ├── pylint.yml │ └── codeql.yml ├── docker-compose.yml ├── README.md ├── .gitignore ├── .editorconfig └── .dockerignore /source/.env: -------------------------------------------------------------------------------- 1 | ../.env -------------------------------------------------------------------------------- /source/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/helper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/interface/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/service/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/controller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/interface/extension/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/controller/extension/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/service/extension/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | -------------------------------------------------------------------------------- /source/.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.join(os.path.dirname(find_pylintrc())))" 3 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.join(os.path.dirname(find_pylintrc()), 'source'))" 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /source/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /source/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /source/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /source/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /source/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main Run file for the project. 3 | """ 4 | import os 5 | 6 | from controller.runner import Runner 7 | 8 | if __name__ == "__main__": 9 | root_path = os.path.dirname(os.path.abspath(__file__)) 10 | Runner(root_path) 11 | -------------------------------------------------------------------------------- /source/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /source/.idea/source.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/selenium-docker.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docker/python/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | ENV LANG C.UTF-8 4 | ENV LC_ALL C.UTF-8 5 | ENV PYTHONUNBUFFERED=1 6 | ENV APP_HOME /usr/src/app 7 | 8 | WORKDIR /$APP_HOME 9 | 10 | COPY ./source/ $APP_HOME/ 11 | COPY ./.env $APP_HOME/ 12 | 13 | RUN apt-get -y update 14 | 15 | RUN pip install --upgrade pip 16 | 17 | RUN pip install -r requirements.txt 18 | 19 | CMD tail -f /dev/null 20 | # CMD python3 main.py -------------------------------------------------------------------------------- /source/requirements.txt: -------------------------------------------------------------------------------- 1 | async-generator==1.10 2 | attrs==21.4.0 3 | certifi==2021.10.8 4 | cffi==1.15.0 5 | charset-normalizer==2.0.12 6 | cryptography==37.0.2 7 | h11==0.13.0 8 | idna==3.3 9 | outcome==1.1.0 10 | pycparser==2.21 11 | pyOpenSSL==22.0.0 12 | PySocks==1.7.1 13 | python-dotenv==0.20.0 14 | PyVirtualDisplay==3.0 15 | requests==2.27.1 16 | selenium==4.1.5 17 | sniffio==1.2.0 18 | sortedcontainers==2.4.0 19 | trio==0.20.0 20 | trio-websocket==0.9.2 21 | urllib3==1.26.9 22 | webdriver-manager==3.5.4 23 | wsproto==1.1.0 24 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Choose an environment (local, docker, remote) 2 | ENVIRONMENT=local 3 | 4 | # Path File of the assets/drivers/etc.. 5 | PATH_ASSETS=assets 6 | 7 | # Choose a Browser (chrome, firefox) 8 | BROWSER=chrome 9 | # Available extensions in Chrome Store 10 | EXTENSIONS_CHROME=["metamask"] 11 | # Available extensions in Firefox Store 12 | #EXTENSIONS_FIREFOX=["metamask"] 13 | 14 | # Posible Routes to ENVIRONMENT=remote 15 | REMOTE_URL=http://selenium-hub:4444/wd/hub 16 | #REMOTE_URL=http://0.0.0.0:4444/wd/hub 17 | #REMOTE_URL=http://localhost:4444/wd/hub 18 | 19 | # If you active Metamask extension, put your credential here. 20 | METAMASK_AUTH=["SECRET", "A B C D E F G H I J K L"] 21 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.10"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install pylint 21 | pip install -r source/requirements.txt 22 | - name: Analysing the code with pylint 23 | run: | 24 | pylint $(git ls-files '*.py') 25 | -------------------------------------------------------------------------------- /source/service/user_agent_browser.py: -------------------------------------------------------------------------------- 1 | """ 2 | User Agent Browser Service 3 | """ 4 | from helper.complement import Complement 5 | 6 | 7 | class UserAgentBrowser: 8 | """ UserAgentBrowser class """ 9 | path_file = None 10 | 11 | def __init__(self, path_asset, browser, driver): 12 | self.path_file = f"{path_asset}/user_agent_{browser}.txt" 13 | self.driver = driver 14 | 15 | def _get_user_agent(self): 16 | """ Get the user agent """ 17 | command = "return navigator.userAgent" 18 | data_user_agent = self.driver.execute_script(command) 19 | Complement.write_file(self.path_file, data_user_agent) 20 | 21 | def exist_user_agent(self): 22 | """ Check if the file exist """ 23 | return Complement.check_file_exist(self.path_file) 24 | 25 | def data_user_agent(self): 26 | """ Return the data of the file """ 27 | if self.exist_user_agent(): 28 | return Complement.read_file(self.path_file) 29 | 30 | self._get_user_agent() 31 | return self.data_user_agent() 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # To execute this docker-compose yml file use `docker-compose -f docker-compose-v3.yml up` 2 | # Add the `-d` flag at the end for detached execution 3 | # To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v3.yml down` 4 | version: "3" 5 | services: 6 | chrome: 7 | image: selenium/node-chrome:4.1.4-20220427 8 | shm_size: 2gb 9 | depends_on: 10 | - selenium-hub 11 | environment: 12 | - SE_EVENT_BUS_HOST=selenium-hub 13 | - SE_EVENT_BUS_PUBLISH_PORT=4442 14 | - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 15 | 16 | # edge: 17 | # image: selenium/node-edge:4.1.4-20220427 18 | # shm_size: 2gb 19 | # depends_on: 20 | # - selenium-hub 21 | # environment: 22 | # - SE_EVENT_BUS_HOST=selenium-hub 23 | # - SE_EVENT_BUS_PUBLISH_PORT=4442 24 | # - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 25 | 26 | firefox: 27 | image: selenium/node-firefox:4.1.4-20220427 28 | shm_size: 2gb 29 | depends_on: 30 | - selenium-hub 31 | environment: 32 | - SE_EVENT_BUS_HOST=selenium-hub 33 | - SE_EVENT_BUS_PUBLISH_PORT=4442 34 | - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 35 | volumes: 36 | - ./source/assets/extensions/firefox:/usr/src/app/assets/extensions/firefox 37 | 38 | selenium-hub: 39 | image: selenium/hub:4.1.4-20220427 40 | container_name: selenium-hub 41 | ports: 42 | - "4442:4442" 43 | - "4443:4443" 44 | - "4444:4444" 45 | 46 | app: 47 | build: 48 | context: . 49 | dockerfile: ./docker/python/Dockerfile 50 | env_file: 51 | - .env 52 | ports: 53 | - "4000:4000" 54 | volumes: 55 | - .env:/usr/src/app/.env 56 | - ./source:/usr/src/app 57 | privileged: true -------------------------------------------------------------------------------- /source/helper/complement.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module - Complement. 3 | """ 4 | import os 5 | import shutil 6 | 7 | 8 | class Complement: 9 | """ Complement class. """ 10 | 11 | @staticmethod 12 | def browser_is_chrome(browser): 13 | """ Check if browser is chrome. """ 14 | return browser == "chrome" 15 | 16 | @staticmethod 17 | def browser_is_firefox(browser): 18 | """ Check if browser is firefox. """ 19 | return browser == "firefox" 20 | 21 | @staticmethod 22 | def check_file_exist(path): 23 | """ Check if file exist. """ 24 | return os.path.isfile(path) 25 | 26 | @staticmethod 27 | def make_folder(path): 28 | """ Create folder. """ 29 | if not os.path.exists(path): 30 | os.makedirs(path) 31 | 32 | @staticmethod 33 | def move_file(src, dest): 34 | """ Move file. """ 35 | shutil.move(src, dest) 36 | 37 | @staticmethod 38 | def write_file(path, content, mode="w", encoding="utf8"): 39 | """ Write file. """ 40 | if mode == 'wb': 41 | # pylint: skip-file 42 | with open(path, mode) as file: 43 | file.write(content) 44 | else: 45 | with open(path, mode, encoding=encoding) as file: 46 | file.write(content) 47 | 48 | @staticmethod 49 | def read_file(path, encoding="utf8"): 50 | """ Read file. """ 51 | with open(path, 'r', encoding=encoding) as file: 52 | return file.read() 53 | 54 | @staticmethod 55 | def convertENVArray(env_array): 56 | """ Convert ENV[] to Array """ 57 | return env_array.replace('[', '').replace(']', '') \ 58 | .replace('"', '') \ 59 | .replace(', ', ',').replace(' ,', ',') \ 60 | .split(',') 61 | -------------------------------------------------------------------------------- /source/interface/interface.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | """ 3 | Interface. 4 | """ 5 | 6 | 7 | def abstractfunc(func): 8 | """ Decorator for abstract functions. """ 9 | func.__isabstract__ = True 10 | return func 11 | 12 | 13 | class Interface(type): 14 | """ Metaclass for interfaces. """ 15 | 16 | def __init__(self, name, bases, namespace): 17 | """ Check that all abstract methods are implemented. """ 18 | for base in bases: 19 | must_implement = getattr(base, "abstract_methods", []) 20 | class_methods = getattr(self, "all_methods", []) 21 | for method in must_implement: 22 | if method not in class_methods: 23 | err_str = """Can't create abstract class {name}! 24 | {name} must implement abstract method {method} 25 | of class {base_class}!""".format( 26 | name=name, method=method, base_class=base.__name__ 27 | ) 28 | raise TypeError(err_str) 29 | 30 | def __new__(metaclass, name, bases, namespace): 31 | interface_methods = Interface._get_abstract_methods(namespace) 32 | namespace["abstract_methods"] = interface_methods 33 | namespace["all_methods"] = Interface._get_all_methods(namespace) 34 | cls = super().__new__(metaclass, name, bases, namespace) 35 | return cls 36 | 37 | def _get_abstract_methods(namespace): 38 | """ Return a list of abstract methods. """ 39 | return [ 40 | name 41 | for name, val in namespace.items() 42 | if callable(val) and getattr(val, "__isabstract__", False) 43 | ] 44 | 45 | def _get_all_methods(namespace): 46 | """ Return a list of all methods. """ 47 | return [name for name, val in namespace.items() if callable(val)] 48 | -------------------------------------------------------------------------------- /source/controller/runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runner for the controller. 3 | """ 4 | import os 5 | 6 | from dotenv import load_dotenv 7 | from pyvirtualdisplay import Display 8 | 9 | from controller.web_driver import WebDriver 10 | 11 | 12 | class Runner: 13 | """ Runner class """ 14 | driver = None 15 | webdriver = None 16 | display = None 17 | 18 | root_path = None 19 | path_asset = None 20 | 21 | browser = 'chrome' 22 | environment = 'local' 23 | 24 | def __init__(self, root_path): 25 | self.root_path = root_path 26 | self.run() 27 | 28 | def run(self): 29 | """ Run """ 30 | self.configuration() 31 | self.configure_browser() 32 | self.test() 33 | 34 | def configuration(self): 35 | """ Configuration """ 36 | load_dotenv() 37 | self.path_asset = os.getenv('PATH_ASSETS') or 'assets' 38 | self.path_asset = self.root_path + '/' + self.path_asset 39 | self.environment = os.getenv('ENVIRONMENT') or 'local' 40 | self.browser = os.getenv('BROWSER') or 'chrome' 41 | 42 | def configure_browser(self): 43 | """ Configure browser """ 44 | if self.environment == 'docker': 45 | self.start_display() 46 | self.start_webdriver() 47 | 48 | def start_webdriver(self): 49 | """ Start webdriver """ 50 | self.webdriver = WebDriver(self.path_asset, self.browser, self.environment) 51 | self.driver = self.webdriver.get_driver() 52 | 53 | def start_display(self): 54 | """ Start display """ 55 | self.display = Display(size=(800, 600)) 56 | self.display.start() 57 | 58 | def stop_display(self): 59 | """ Stop display """ 60 | self.display.stop() 61 | 62 | def test(self): 63 | """ Test """ 64 | self.webdriver.start() 65 | if self.environment == 'docker': 66 | self.stop_display() 67 | -------------------------------------------------------------------------------- /source/interface/extension/extension_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extension Manager 3 | """ 4 | from abc import abstractmethod 5 | 6 | import requests 7 | 8 | from helper.complement import Complement 9 | from interface.interface import Interface 10 | 11 | 12 | class ExtensionManager(metaclass=Interface): 13 | """ Extension Manager Interface """ 14 | path_file = None 15 | url_extension_firefox = None 16 | 17 | def __init__(self): 18 | self.driver = None 19 | 20 | # @abstractmethod 21 | def set_driver(self, driver): 22 | """ Set driver """ 23 | self.driver = driver 24 | 25 | @abstractmethod 26 | def _get_info(self, url_base_extension): 27 | """ Get info extension """ 28 | data_main = url_base_extension.split("/") 29 | file_name = data_main[-1] 30 | return {"file_name": file_name} 31 | 32 | @abstractmethod 33 | def generate_extension(self, url): 34 | """ Generate extension """ 35 | # info_extension = self._get_info(url) 36 | # path_file = self._get_path_extension(url, info_extension) 37 | # self._download_extension(url, path_file) 38 | # return path_file 39 | 40 | # @abstractmethod 41 | def _get_path_extension(self, url_chrome_extension, info_extension=None): 42 | """ Get path extension """ 43 | if info_extension is None: 44 | info_extension = self._get_info(url_chrome_extension) 45 | Complement.make_folder(self.path_file) 46 | path_file = f"{self.path_file}/{info_extension['file_name']}" 47 | return path_file 48 | 49 | # @abstractmethod 50 | def set_extension_path(self, url_base_extension): 51 | """ Set extension path """ 52 | path_file = self._get_path_extension(url_base_extension) 53 | return path_file 54 | 55 | # @abstractmethod 56 | def exist_extension_by_url(self, url_base_extension): 57 | """ Exist extension by url """ 58 | path_extension = self.set_extension_path(url_base_extension) 59 | return { 60 | "path_file": path_extension, 61 | "exist": Complement.check_file_exist(path_extension), 62 | } 63 | 64 | # @abstractmethod 65 | @staticmethod 66 | def _download_extension(url, path_file): 67 | """ Download extension """ 68 | request_uri = requests.get(url) 69 | content = request_uri.content 70 | Complement.write_file(path_file, content, 'wb') 71 | -------------------------------------------------------------------------------- /source/service/extension/download_extension_firefox.py: -------------------------------------------------------------------------------- 1 | """ 2 | Download Extension - Firefox 3 | """ 4 | import requests 5 | 6 | from interface.extension.extension_manager import ExtensionManager 7 | 8 | 9 | def get_main_url(url_extension, name_extension): 10 | """ Get Main Url """ 11 | response = requests.get(url_extension) 12 | response_json = response.json() 13 | url_extension = "" 14 | for item in response_json["results"]: 15 | if item["slug"] == name_extension: 16 | url_extension = item["current_version"]["file"]["url"] 17 | break 18 | return url_extension 19 | 20 | 21 | class DownloadExtensionFirefox(ExtensionManager): 22 | """ Download Extensions Firefox """ 23 | 24 | path_file = "extensions/firefox" 25 | # Firefox 26 | url_extension_firefox = ( 27 | "https://addons.mozilla.org/api/v5/addons/search/" 28 | "?app=firefox&author={user_ids}" 29 | "&exclude_addons={name_extension}" 30 | "&page=1&page_size=10&sort=hotness" 31 | "&type=extension&lang=en-US" 32 | ) 33 | 34 | def __init__(self, path_assets): 35 | super().__init__() 36 | self.path_file = f"{path_assets}/{self.path_file}" 37 | 38 | def get_base_url(self, object_url): 39 | """ Get Base Url """ 40 | name_extension = object_url["collection_url"].split("/")[-1] 41 | user_ids = "%2C".join(str(x) for x in object_url["user_id"]) 42 | url_extension = self.url_extension_firefox.format( 43 | user_ids=user_ids, name_extension=name_extension 44 | ) 45 | return url_extension 46 | 47 | def get_url_extension(self, object_url): 48 | """ Get Url Extensions""" 49 | url_extension = self.get_base_url(object_url) 50 | name_real_extension = object_url["url"].split("/")[-1] 51 | main_url = get_main_url(url_extension, name_real_extension) 52 | return main_url 53 | 54 | # Override 55 | def _get_info(self, url_base_extension): 56 | """ Get Info """ 57 | data_main = url_base_extension.split("/") 58 | file_name = data_main[-1] 59 | return {"file_name": file_name} 60 | 61 | def generate_extension(self, url): 62 | """ Generate extension """ 63 | info_extension = self._get_info(url) 64 | path_file = self._get_path_extension(url, info_extension) 65 | self._download_extension(url, path_file) 66 | return path_file 67 | -------------------------------------------------------------------------------- /source/controller/extension_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extension Manager for the controller. 3 | """ 4 | import os 5 | 6 | from controller.extension.captchasolver import CaptchaSolver 7 | from controller.extension.metamask import MetaMask 8 | from helper.complement import Complement 9 | 10 | 11 | class ExtensionManager: 12 | """ Extension Manager for the controller. """ 13 | path_assets = None 14 | browser = None 15 | driver_action = None 16 | driver = None 17 | wallet = None 18 | captcha = None 19 | extensions = [] 20 | 21 | def __init__(self, path_assets, browser, driver_action=None, driver=None): 22 | self.path_assets = path_assets 23 | self.browser = browser 24 | self.driver_action = driver_action 25 | self.driver = driver 26 | self.set_extensions() 27 | 28 | def get_extensions(self): 29 | """ Get the extensions """ 30 | return self.extensions 31 | 32 | def set_extensions(self): 33 | """ Set the extensions """ 34 | self.extensions = [] 35 | 36 | extensions = os.getenv('EXTENSIONS_' + self.browser.upper()) 37 | if extensions is not None: 38 | extensions = Complement.convertENVArray(extensions) 39 | 40 | for extension in extensions: 41 | match extension: 42 | case 'metamask': 43 | self.start_wallet() 44 | case 'captcha': 45 | self.start_captcha() 46 | 47 | def start_captcha(self): 48 | """ Start the captcha """ 49 | self.set_captcha() 50 | if self.captcha is not None: 51 | self.extensions.append(self.captcha.get_path_extension()) 52 | 53 | def set_captcha(self): 54 | """ Set the captcha instance """ 55 | self.captcha = CaptchaSolver(self.path_assets, self.browser, 56 | self.driver_action, self.driver) 57 | self.captcha.start() 58 | 59 | def get_captcha(self): 60 | """ Get the captcha instance """ 61 | return self.captcha 62 | 63 | def start_wallet(self): 64 | """ Start the wallet """ 65 | self.set_wallet() 66 | if self.wallet is not None: 67 | self.extensions.append(self.wallet.get_path_extension()) 68 | 69 | def set_wallet(self): 70 | """ Set the wallet instance """ 71 | self.wallet = MetaMask(self.path_assets, self.browser, 72 | self.driver_action, self.driver) 73 | self.wallet.start() 74 | 75 | def get_waller(self): 76 | """ Get the wallet instance """ 77 | return self.wallet 78 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '44 0 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /source/interface/extension/plugin_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plugin Manager 3 | """ 4 | from helper.complement import Complement 5 | from interface.interface import Interface 6 | from service.extension.download_extension_chrome import DownloadExtensionChrome 7 | from service.extension.download_extension_firefox import DownloadExtensionFirefox 8 | 9 | 10 | class PluginManager(metaclass=Interface): 11 | """ Plugin manager class """ 12 | path_assets = "" 13 | path_data = "" 14 | # Chrome 15 | url_extension = "" 16 | # Firefox 17 | data_extension_firefox = {} 18 | 19 | def __init__(self, path_assets, browser, driver=None): 20 | self.path_assets = path_assets 21 | self.browser = browser 22 | 23 | self.driver = driver 24 | 25 | self.download_extension = None 26 | self.set_config() 27 | 28 | # @abstractmethod 29 | def set_config(self): 30 | """ Set config """ 31 | self.set_browser() 32 | self.set_driver(self.driver) 33 | 34 | # @abstractmethod 35 | def set_browser(self): 36 | """ Set browser """ 37 | if self.browser_is_chrome(): 38 | self.download_extension = DownloadExtensionChrome(self.path_assets) 39 | elif self.browser_is_firefox(): 40 | self.download_extension = DownloadExtensionFirefox(self.path_assets) 41 | 42 | # @abstractmethod 43 | def browser_is_chrome(self): 44 | """ Check if browser is chrome """ 45 | return Complement.browser_is_chrome(self.browser) 46 | 47 | # @abstractmethod 48 | def browser_is_firefox(self): 49 | """ Check if browser is firefox """ 50 | return Complement.browser_is_firefox(self.browser) 51 | 52 | # @abstractmethod 53 | def set_driver(self, driver): 54 | """ Set driver """ 55 | self.driver = driver 56 | self.download_extension.set_driver(self.driver) 57 | 58 | # @abstractmethod 59 | def get_information_extension(self): 60 | """ Get information extension """ 61 | url = self.url_extension 62 | return self.download_extension.exist_extension_by_url(url) 63 | 64 | # @abstractmethod 65 | def get_path_extension(self): 66 | """ Get path extension """ 67 | check_extension = self.get_information_extension() 68 | return check_extension["path_file"] 69 | 70 | # @abstractmethod 71 | def extension_exist(self): 72 | """ Check if extension exist """ 73 | check_extension = self.get_information_extension() 74 | return check_extension["exist"] 75 | 76 | # @abstractmethod 77 | def install(self): 78 | """ Install extension """ 79 | if not self.extension_exist(): 80 | self.download_extension.generate_extension(self.url_extension) 81 | 82 | # @abstractmethod 83 | def start(self): 84 | """ Start extension """ 85 | if self.browser_is_firefox(): 86 | self.url_extension = self.download_extension.get_url_extension( 87 | self.data_extension_firefox 88 | ) 89 | self.install() 90 | -------------------------------------------------------------------------------- /docker/single/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:focal 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | ENV DISPLAY=:99 6 | ENV LANG C.UTF-8 7 | ENV LC_ALL C.UTF-8 8 | ENV PYTHONUNBUFFERED=1 9 | ENV PYTHON_VERSION_PROJECT 3.10 10 | ENV APP_HOME /usr/src/app 11 | 12 | WORKDIR /$APP_HOME 13 | 14 | # Install Python Version 3.10 15 | RUN apt-get update -y 16 | 17 | RUN apt-get install -y software-properties-common curl unzip wget 18 | RUN apt-add-repository ppa:deadsnakes/ppa 19 | RUN apt-get update -y 20 | 21 | RUN apt-get install -y python3.10 22 | RUN apt-get install -y python3.10-distutils 23 | 24 | RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1 25 | RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 2 26 | RUN update-alternatives --config python3 27 | 28 | RUN python3 --version 29 | 30 | RUN apt-get install -y python3-pip 31 | RUN apt-get install -y curl 32 | 33 | RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10 34 | RUN pip install --upgrade pip 35 | 36 | # Install Browser 37 | RUN apt-get install --no-install-recommends -yqq \ 38 | ca-certificates \ 39 | gnupg2 \ 40 | software-properties-common 41 | 42 | # install google chrome 43 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - 44 | RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' 45 | RUN apt-get -y update 46 | RUN apt-get install -y google-chrome-stable 47 | 48 | # install chromedriver 49 | RUN apt-get install -yqq unzip 50 | RUN wget -O /tmp/chromedriver.zip http://chromedriver.storage.googleapis.com/`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE`/chromedriver_linux64.zip 51 | RUN unzip /tmp/chromedriver.zip chromedriver -d /usr/local/bin/ 52 | RUN chmod +x /usr/local/bin/chromedriver 53 | RUN rm /tmp/chromedriver.zip 54 | 55 | RUN apt-get install --no-install-recommends -y \ 56 | fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 \ 57 | libnspr4 libnss3 lsb-release xdg-utils libxss1 libdbus-glib-1-2 libgbm1 \ 58 | xvfb 59 | 60 | 61 | # Install firefox 62 | ENV FIREFOX_SETUP=firefox-setup.tar.bz2 63 | RUN apt-get purge firefox 64 | RUN wget -O $FIREFOX_SETUP "https://download.mozilla.org/?product=firefox-latest&os=linux64" && \ 65 | tar xjf $FIREFOX_SETUP -C /opt/ && \ 66 | ln -s /opt/firefox/firefox /usr/bin/firefox && \ 67 | rm $FIREFOX_SETUP 68 | 69 | RUN echo "GECKODRIVER_VERSION="$(curl -s https://api.github.com/repos/mozilla/geckodriver/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")') > /tmp/geckodriver_version 70 | RUN export $(cat /tmp/geckodriver_version) && \ 71 | wget https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz && \ 72 | tar -zxf geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz -C /usr/local/bin && \ 73 | chmod +x /usr/local/bin/geckodriver && \ 74 | rm geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz && \ 75 | rm /tmp/geckodriver_version 76 | 77 | # Generate App 78 | COPY ./source/ $APP_HOME/ 79 | COPY .env $APP_HOME/ 80 | RUN [ -f /$APP_HOME/.env ] || cp .env /$APP_HOME/ 81 | 82 | RUN pip install -r requirements.txt 83 | 84 | # set display port to avoid crash 85 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /source/service/driver_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Driver Manager 3 | """ 4 | from webdriver_manager.chrome import ChromeDriverManager 5 | from webdriver_manager.firefox import GeckoDriverManager 6 | 7 | from helper.complement import Complement 8 | from service.driver.configuration_chrome import ConfigurationChrome 9 | from service.driver.configuration_firefox import ConfigurationFirefox 10 | 11 | 12 | def download_chrome_driver(): 13 | """ Download Chrome Driver """ 14 | return ChromeDriverManager().install() 15 | 16 | 17 | def download_firefox_driver(): 18 | """ Download Firefox Driver """ 19 | return GeckoDriverManager().install() 20 | 21 | 22 | class DriverManager: 23 | """ Driver Manager """ 24 | driver = None 25 | path_driver = None 26 | path_assets = None 27 | 28 | environment = 'local' 29 | 30 | browser = 'chrome' 31 | is_chrome = False 32 | is_firefox = False 33 | configuration_driver = None 34 | 35 | driver_chrome = "chromedriver" 36 | driver_firefox = "geckodriver" 37 | 38 | def __init__(self, path_assets, browser, environment): 39 | self.path_assets = path_assets 40 | self.browser = browser 41 | self.environment = environment 42 | self.init() 43 | 44 | def init(self): 45 | """ Initialize """ 46 | self.is_chrome = Complement.browser_is_chrome(self.browser) 47 | self.is_firefox = Complement.browser_is_firefox(self.browser) 48 | self.path_driver = f"{self.path_assets}/driver/" 49 | self._set_path_driver() 50 | self.configure_driver() 51 | 52 | def _set_path_driver(self): 53 | """ Set Path Driver """ 54 | driver_name = None 55 | if self.is_chrome: 56 | driver_name = self.driver_chrome 57 | elif self.is_firefox: 58 | driver_name = self.driver_firefox 59 | 60 | Complement.make_folder(self.path_driver) 61 | self.path_driver = self.path_driver + driver_name 62 | 63 | def setup_driver(self): 64 | """ Setup Driver """ 65 | if not Complement.check_file_exist(self.path_driver) and self.environment == 'local': 66 | self.download_driver() 67 | 68 | def download_driver(self): 69 | """ Download Driver """ 70 | driver_data = None 71 | if self.is_chrome: 72 | driver_data = download_chrome_driver() 73 | elif self.is_firefox: 74 | driver_data = download_firefox_driver() 75 | 76 | if driver_data is not None: 77 | Complement.move_file(driver_data, self.path_driver) 78 | 79 | def configure_driver(self): 80 | """ Configure Driver """ 81 | config_single = None 82 | if self.is_chrome: 83 | config_single = ConfigurationChrome( 84 | self.path_driver, self.path_assets, self.environment 85 | ) 86 | elif self.is_firefox: 87 | config_single = ConfigurationFirefox( 88 | self.path_driver, self.path_assets, self.environment 89 | ) 90 | 91 | self.configuration_driver = config_single 92 | 93 | def configure_single(self): 94 | """ Configure Single """ 95 | self.configuration_driver.set_driver() 96 | 97 | def get_driver(self): 98 | """ Get Driver """ 99 | return self.configuration_driver.get_driver() 100 | 101 | def configure_master(self, path_extensions): 102 | """ Configure Master """ 103 | self.configuration_driver.set_driver_extension(path_extensions) 104 | -------------------------------------------------------------------------------- /source/service/driver/configuration_firefox.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | """ 3 | Configuration Firefox driver 4 | """ 5 | from selenium import webdriver 6 | from selenium.webdriver.firefox.service import Service as ServiceFirefox 7 | 8 | from helper.complement import Complement 9 | from interface.driver_interface import DriverInterface 10 | 11 | 12 | class ConfigurationFirefox(DriverInterface): 13 | """ Configuration Firefox driver """ 14 | 15 | def __init__(self, path_driver, path_assets, environment): 16 | super().__init__(path_driver, path_assets, environment) 17 | 18 | def set_driver_extension(self, path_extensions): 19 | """ Set driver extension """ 20 | options_browser = self._get_options() 21 | options_browser = self._get_extension_pre_config(options_browser) 22 | # options_browser = self._sign_extension(options_browser) 23 | self.driver = self._get_manager_driver(options_browser) 24 | self._set_extension_post_config(path_extensions) 25 | 26 | def _get_extension_pre_config(self, options_browser): 27 | """ Get extension pre config """ 28 | options_browser.accept_untrusted_certs = True 29 | options_browser.accept_insecure_certs = True 30 | options_browser.set_preference('xpinstall.signatures.required', False) 31 | options_browser.set_preference('security.fileuri.strict_origin_policy', False) 32 | options_browser.set_preference("plugin.state.flash", 2) 33 | # options_browser.add_argument('--disable-popup-blocking') 34 | 35 | return options_browser 36 | 37 | def _set_extension_post_config(self, path_extensions): 38 | """ Set extension post config """ 39 | for path_extension in path_extensions: 40 | self._set_manager_extension_post_config(path_extension) 41 | 42 | def _set_manager_extension_post_config(self, path_extension): 43 | """ Set manager extension post config """ 44 | match self.environment: 45 | case 'local': 46 | self._set_extension_post_local(path_extension) 47 | case 'docker': 48 | self._set_extension_post_docker(path_extension) 49 | case 'remote': 50 | self._set_extension_post_remote(path_extension) 51 | 52 | def _set_extension_post_local(self, path_extension): 53 | """ Set extension post local """ 54 | self.driver.install_addon(path_extension) 55 | 56 | def _set_extension_post_docker(self, path_extension): 57 | """ Set extension post docker """ 58 | self.driver.install_addon(path_extension) 59 | 60 | def _set_extension_post_remote(self, path_extension): 61 | """ Set extension post remote """ 62 | payload = {"path": path_extension, "temporary": True} 63 | # The function returns an identifier of the installed addon. 64 | var = self.driver.execute("INSTALL_ADDON", payload)["value"] 65 | # This identifier can later be used to uninstall installed addon. 66 | print(var) 67 | 68 | def _get_driver_local(self, options_browser): 69 | """ Get driver local """ 70 | service = self._get_service() 71 | return webdriver.Firefox(service=service, options=options_browser) 72 | 73 | def _get_driver_docker(self, options_browser): 74 | """ Get driver docker """ 75 | return webdriver.Firefox(options=options_browser) 76 | 77 | def _get_service(self): 78 | """ Get service """ 79 | log_path = f"{self.path_assets}/log" 80 | Complement.make_folder(log_path) 81 | 82 | return ServiceFirefox(executable_path=self.path_driver, log_path=f"{log_path}/firefox.log") 83 | 84 | def _set_service_option(self): 85 | """ Set service option """ 86 | return webdriver.FirefoxOptions() 87 | -------------------------------------------------------------------------------- /source/service/extension/download_extension_chrome.py: -------------------------------------------------------------------------------- 1 | """ 2 | Download Extension Chrome 3 | """ 4 | import re 5 | 6 | from interface.extension.extension_manager import ExtensionManager 7 | from service.user_agent_browser import UserAgentBrowser 8 | 9 | 10 | def _get_version(user_agent): 11 | """ Get version """ 12 | response = {"major": "", "minor": "", "build": "", "patch": ""} 13 | # Get Version 14 | regex = r"Chrom(?:e|ium)\/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)" 15 | matches = re.finditer(regex, user_agent, re.MULTILINE) 16 | for match_num, match in enumerate(matches, start=1): 17 | match_value = match.group() 18 | print(match_num) 19 | 20 | if match_value: 21 | code_value = match_value.split("/")[-1] 22 | if code_value: 23 | value = code_value.split(".") 24 | if len(value) == 4: 25 | response = { 26 | "major": value[0], 27 | "minor": value[1], 28 | "build": value[2], 29 | "patch": value[3], 30 | } 31 | # Make Again version 32 | version = ( 33 | response["major"] 34 | + "." 35 | + response["minor"] 36 | + "." 37 | + response["build"] 38 | + "." 39 | + response["patch"] 40 | ) 41 | # Return version 42 | return version 43 | 44 | 45 | def _get_arch(user_agent): 46 | """ Get arch """ 47 | nacl_arch = "arm" 48 | if user_agent.find("x86") > 0: 49 | nacl_arch = "x86-32" 50 | elif user_agent.find("x64") > 0: 51 | nacl_arch = "x86-64" 52 | return nacl_arch 53 | 54 | 55 | class DownloadExtensionChrome(ExtensionManager): 56 | """ Class for downloading extensions for Chrome """ 57 | extension = "crx" 58 | path_file = "extensions/chrome" 59 | path_assets = None 60 | browser = 'chrome' 61 | driver = None 62 | 63 | MAIN_URL = "https://clients2.google.com/service/update2/crx?response=redirect&" \ 64 | "prodversion={version}&acceptformat=crx2,crx3" \ 65 | "&x=id%3D{id_extension}%26uc" \ 66 | "&nacl_arch={nacl_arch}" 67 | 68 | def __init__(self, path_assets): 69 | super().__init__() 70 | self.path_assets = path_assets 71 | self.path_file = f"{path_assets}/{self.path_file}" 72 | 73 | def _get_user_agent_browser(self): 74 | """ Get user agent browser """ 75 | user_agent_browser = UserAgentBrowser(self.path_assets, self.browser, self.driver) 76 | return user_agent_browser.data_user_agent() 77 | 78 | def _make_url(self, id_extension): 79 | """ Make url """ 80 | user_agent = self._get_user_agent_browser() 81 | version = _get_version(user_agent) 82 | nacl_arch = _get_arch(user_agent) 83 | url = (self.MAIN_URL.format( 84 | version=version, id_extension=id_extension, nacl_arch=nacl_arch 85 | ) 86 | ) 87 | return url 88 | 89 | def _get_info(self, url_base_extension): 90 | """ Get info extension """ 91 | data_main = url_base_extension.split("/") 92 | extension_id = data_main[-1] 93 | extension_name = data_main[-2] 94 | file_name = f"{extension_name}.{self.extension}" 95 | return { 96 | "extension_id": extension_id, 97 | "extension_name": extension_name, 98 | "file_name": file_name, 99 | } 100 | 101 | def _get_data_extension(self, url_ext): 102 | """ Get data extension """ 103 | info_extension = self._get_info(url_ext) 104 | path_file = self._get_path_extension(url_ext, info_extension) 105 | url = self._make_url(info_extension["extension_id"]) 106 | return {"url": url, "path_file": path_file} 107 | 108 | def generate_extension(self, url): 109 | """ Generate extension """ 110 | data = self._get_data_extension(url) 111 | url = data["url"] 112 | path_file = data["path_file"] 113 | self._download_extension(url, path_file) 114 | return data["path_file"] 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker + Docker-compose + Selenium + Python 2 | 3 |
4 | 5 |
6 | 7 | This is a simple example of how to use Docker, Docker-compose and Selenium together to run a simple Python script. 8 | 9 | In this case, we can use three different ways to run the script: 10 | 11 | 1. Native 12 | 2. Dockerfile 13 | 3. Docker-compose with Simulated Web Driver 14 | 15 | ## Native 16 | 17 | To run the script natively, we can use the next commands. 18 | 19 | 1. You need to install Chrome or Firefox. 20 | 21 | 2. Start the workspace. 22 | 23 | ```shell 24 | virtualenv venv 25 | source venv/bin/activate 26 | ``` 27 | 28 | 3. Edit the file .env to the workspace. 29 | 30 | As example, can be `source/.env` 31 | 32 | ```dotenv 33 | ENVIRONMENT=local 34 | #ENVIRONMENT=docker 35 | #ENVIRONMENT=remote 36 | 37 | PATH_ASSETS=assets 38 | 39 | #BROWSER=chrome 40 | #EXTENSIONS_CHROME=["metamask"] 41 | 42 | BROWSER=firefox 43 | EXTENSIONS_FIREFOX=["metamask", "captcha"] 44 | 45 | REMOTE_URL=http://selenium-hub:4444/wd/hub 46 | #REMOTE_URL=http://0.0.0.0:4444/wd/hub 47 | #REMOTE_URL=http://localhost:4444/wd/hub 48 | 49 | METAMASK_AUTH=["SECRET", "A B C D E F G H I J K L"] 50 | ``` 51 | 52 | 4. Inside to the folder `source` 53 | 54 | ```shell 55 | cd source 56 | ``` 57 | 58 | 5. Install the dependencies. 59 | 60 | ```shell 61 | python -m pip install --upgrade pip 62 | pip install -r requirements.txt 63 | ``` 64 | 65 | 6. Execute the script. 66 | 67 | ```shell 68 | python main.py 69 | ``` 70 | 71 | 7. Stop the workspace. 72 | 73 | ```shell 74 | deactivate 75 | ``` 76 | 77 | ## Dockerfile 78 | 79 | 1. Edit the file .env to the workspace. 80 | 81 | As example, can be `source/.env` 82 | 83 | ```dotenv 84 | #ENVIRONMENT=local 85 | ENVIRONMENT=docker 86 | #ENVIRONMENT=remote 87 | 88 | PATH_ASSETS=assets 89 | 90 | BROWSER=chrome 91 | EXTENSIONS_CHROME=["metamask", "captcha"] 92 | 93 | #BROWSER=firefox 94 | #EXTENSIONS_FIREFOX=["metamask"] 95 | 96 | REMOTE_URL=http://selenium-hub:4444/wd/hub 97 | #REMOTE_URL=http://0.0.0.0:4444/wd/hub 98 | #REMOTE_URL=http://localhost:4444/wd/hub 99 | 100 | METAMASK_AUTH=["SECRET", "A B C D E F G H I J K L"] 101 | 102 | ``` 103 | 104 | 2. Build the Docker image. 105 | 106 | ```shell 107 | docker build -f docker/single/Dockerfile -t selenium-python-docker . 108 | ``` 109 | 110 | 3. Run the Docker image. 111 | 112 | ```shell 113 | docker run -it --rm -v $(pwd)/source:/usr/src/app -w /usr/src/app --env-file=.env selenium-python-docker 114 | ``` 115 | 116 | ## Docker-compose 117 | 118 | 1. Edit the file .env to the workspace. 119 | 120 | As example, can be `source/.env` 121 | 122 | ```dotenv 123 | #ENVIRONMENT=local 124 | #ENVIRONMENT=docker 125 | ENVIRONMENT=remote 126 | 127 | PATH_ASSETS=assets 128 | 129 | BROWSER=chrome 130 | EXTENSIONS_CHROME=["metamask"] 131 | 132 | #BROWSER=firefox 133 | #EXTENSIONS_FIREFOX=["metamask"] 134 | 135 | REMOTE_URL=http://selenium-hub:4444/wd/hub 136 | #REMOTE_URL=http://0.0.0.0:4444/wd/hub 137 | #REMOTE_URL=http://localhost:4444/wd/hub 138 | 139 | METAMASK_AUTH=["SECRET", "A B C D E F G H I J K L"] 140 | 141 | ``` 142 | 143 | 2. Build the Docker image. 144 | 145 | ```shell 146 | docker-compose build 147 | ``` 148 | 149 | 3. Up the Docker image. 150 | 151 | ```shell 152 | docker-compose up 153 | ``` 154 | 155 | 4. Execute the script in docker service. 156 | 157 | ```shell 158 | docker-compose exec -it app bash 159 | ``` 160 | 161 | Or execute the script in the container. 162 | 163 | ```shell 164 | docker exec -it $(docker ps | grep app | awk '{print $1}') bash 165 | # python main.py 166 | ``` 167 | 168 | 5. In the localhost:4000, you can see the result in session, note: the password of session vnc is `secret` 169 | 170 | 6. Stop the Docker image. 171 | 172 | ```shell 173 | docker-compose down 174 | ``` 175 | -------------------------------------------------------------------------------- /source/interface/driver_interface.py: -------------------------------------------------------------------------------- 1 | """ 2 | Driver interface for the robot. 3 | """ 4 | import json 5 | import os 6 | import uuid 7 | from abc import abstractmethod 8 | 9 | from selenium import webdriver 10 | 11 | from interface.interface import Interface 12 | 13 | 14 | class DriverInterface(metaclass=Interface): 15 | """ Driver interface for the robot. """ 16 | driver = None 17 | path_driver = None 18 | path_assets = None 19 | environment = 'local' 20 | 21 | def __init__(self, path_driver, path_assets, environment): 22 | self.path_driver = path_driver 23 | self.path_assets = path_assets 24 | self.environment = environment 25 | 26 | @abstractmethod 27 | def set_driver(self): 28 | """ Set driver for the browser. """ 29 | options_browser = self._get_options() 30 | self.driver = self._get_manager_driver(options_browser) 31 | 32 | @abstractmethod 33 | def set_driver_extension(self, path_extensions): 34 | """ Set driver for the browser. """ 35 | options_browser = self._get_options() 36 | options_browser = self._get_extension_pre_config(options_browser) 37 | # options_browser = self._sign_extension(options_browser) 38 | self.driver = self._get_manager_driver(options_browser) 39 | 40 | @abstractmethod 41 | def _get_extension_pre_config(self, options_browser): 42 | """ Set driver for the browser. """ 43 | options_browser.accept_untrusted_certs = True 44 | options_browser.accept_insecure_certs = True 45 | return options_browser 46 | 47 | @abstractmethod 48 | def _sign_extension(self, options_browser): 49 | """ Sign extension. """ 50 | addon_id = "webextension@metamask.io" 51 | json_info = json.dumps({addon_id: str(uuid.uuid4())}) 52 | options_browser.set_preference("extensions.webextensions.uuids", json_info) 53 | return options_browser 54 | 55 | @abstractmethod 56 | def get_driver(self): 57 | """ Get driver for the browser. """ 58 | return self.driver 59 | 60 | @abstractmethod 61 | def _get_manager_driver(self, options_browser): 62 | match self.environment: 63 | case 'local': 64 | return self._get_driver_local(options_browser) 65 | case 'docker': 66 | return self._get_driver_docker(options_browser) 67 | case 'remote': 68 | return self._get_driver_remote(options_browser) 69 | case default: 70 | return None 71 | 72 | @abstractmethod 73 | def _get_driver_local(self, options_browser): 74 | service = self._get_service() 75 | # webdriver.Firefox or webdriver.Chrome 76 | 77 | @abstractmethod 78 | def _get_driver_docker(self, options_browser): 79 | # webdriver.Firefox or webdriver.Chrome 80 | return None 81 | 82 | @abstractmethod 83 | def _get_driver_remote(self, options_browser): 84 | remote_url = os.getenv("REMOTE_URL") or "http://selenium-hub:4444/wd/hub" 85 | return webdriver.Remote( 86 | command_executor=remote_url, 87 | options=options_browser 88 | ) 89 | 90 | @abstractmethod 91 | def _get_service(self): 92 | # ServiceFirefox or ServiceChrome 93 | return None 94 | 95 | @abstractmethod 96 | def _get_options(self): 97 | options_browser = self._set_service_option() 98 | options_browser = self._get_options_general(options_browser) 99 | options_browser = self._get_manager_options(options_browser) 100 | return options_browser 101 | 102 | @abstractmethod 103 | def _set_service_option(self): 104 | # return webdriver.FirefoxOptions() webdriver.ChromeOptions() 105 | return None 106 | 107 | @abstractmethod 108 | def _get_manager_options(self, options_browser): 109 | match self.environment: 110 | case 'local': 111 | return self._get_options_local(options_browser) 112 | case 'docker': 113 | return self._get_options_docker(options_browser) 114 | case 'remote': 115 | return self._get_options_remote(options_browser) 116 | case default: 117 | return options_browser 118 | 119 | @abstractmethod 120 | def _get_options_general(self, options_browser): 121 | options_browser.add_argument("--lang=en-US") 122 | options_browser.add_argument("--disable-infobars") 123 | return options_browser 124 | 125 | @abstractmethod 126 | def _get_options_local(self, options_browser): 127 | options_browser.add_argument("--start-maximized") 128 | options_browser.add_argument("--disk-cache-size=1") 129 | options_browser.add_argument("--media-cache-size=1") 130 | options_browser.add_argument("--disable-application-cache") 131 | return options_browser 132 | 133 | @abstractmethod 134 | def _get_options_docker(self, options_browser): 135 | options_browser.add_argument("--headless") 136 | return options_browser 137 | 138 | @abstractmethod 139 | def _get_options_remote(self, options_browser): 140 | return options_browser 141 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 4 | 5 | # User-specific stuff 6 | .idea/**/workspace.xml 7 | .idea/**/tasks.xml 8 | .idea/**/usage.statistics.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # Generated files 13 | .idea/**/contentModel.xml 14 | 15 | # Sensitive or high-churn files 16 | .idea/**/dataSources/ 17 | .idea/**/dataSources.ids 18 | .idea/**/dataSources.local.xml 19 | .idea/**/sqlDataSources.xml 20 | .idea/**/dynamic.xml 21 | .idea/**/uiDesigner.xml 22 | .idea/**/dbnavigator.xml 23 | 24 | # Gradle 25 | .idea/**/gradle.xml 26 | .idea/**/libraries 27 | 28 | # Gradle and Maven with auto-import 29 | # When using Gradle or Maven with auto-import, you should exclude module files, 30 | # since they will be recreated, and may cause churn. Uncomment if using 31 | # auto-import. 32 | # .idea/artifacts 33 | # .idea/compiler.xml 34 | # .idea/jarRepositories.xml 35 | # .idea/modules.xml 36 | # .idea/*.iml 37 | # .idea/modules 38 | # *.iml 39 | # *.ipr 40 | 41 | # CMake 42 | cmake-build-*/ 43 | 44 | # Mongo Explorer plugin 45 | .idea/**/mongoSettings.xml 46 | 47 | # File-based project format 48 | *.iws 49 | 50 | # IntelliJ 51 | out/ 52 | 53 | # mpeltonen/sbt-idea plugin 54 | .idea_modules/ 55 | 56 | # JIRA plugin 57 | atlassian-ide-plugin.xml 58 | 59 | # Cursive Clojure plugin 60 | .idea/replstate.xml 61 | 62 | # Crashlytics plugin (for Android Studio and IntelliJ) 63 | com_crashlytics_export_strings.xml 64 | crashlytics.properties 65 | crashlytics-build.properties 66 | fabric.properties 67 | 68 | # Editor-based Rest Client 69 | .idea/httpRequests 70 | 71 | # Android studio 3.1+ serialized cache file 72 | .idea/caches/build_file_checksums.ser 73 | 74 | ### VirtualEnv template 75 | # Virtualenv 76 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 77 | .Python 78 | [Bb]in 79 | [Ii]nclude 80 | [Ll]ib 81 | [Ll]ib64 82 | [Ll]ocal 83 | [Ss]cripts 84 | pyvenv.cfg 85 | .venv 86 | pip-selfcheck.json 87 | 88 | ### macOS template 89 | # General 90 | .DS_Store 91 | .AppleDouble 92 | .LSOverride 93 | 94 | # Icon must end with two \r 95 | Icon 96 | 97 | # Thumbnails 98 | ._* 99 | 100 | # Files that might appear in the root of a volume 101 | .DocumentRevisions-V100 102 | .fseventsd 103 | .Spotlight-V100 104 | .TemporaryItems 105 | .Trashes 106 | .VolumeIcon.icns 107 | .com.apple.timemachine.donotpresent 108 | 109 | # Directories potentially created on remote AFP share 110 | .AppleDB 111 | .AppleDesktop 112 | Network Trash Folder 113 | Temporary Items 114 | .apdisk 115 | 116 | ### Python template 117 | # Byte-compiled / optimized / DLL files 118 | __pycache__/ 119 | **/__pycache__/ 120 | *.py[cod] 121 | *$py.class 122 | 123 | # C extensions 124 | *.so 125 | 126 | # Distribution / packaging 127 | .Python 128 | build/ 129 | develop-eggs/ 130 | dist/ 131 | downloads/ 132 | eggs/ 133 | .eggs/ 134 | lib/ 135 | lib64/ 136 | parts/ 137 | sdist/ 138 | var/ 139 | wheels/ 140 | share/python-wheels/ 141 | *.egg-info/ 142 | .installed.cfg 143 | *.egg 144 | MANIFEST 145 | 146 | # PyInstaller 147 | # Usually these files are written by a python script from a template 148 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 149 | *.manifest 150 | *.spec 151 | 152 | # Installer logs 153 | pip-log.txt 154 | pip-delete-this-directory.txt 155 | 156 | # Unit test / coverage reports 157 | htmlcov/ 158 | .tox/ 159 | .nox/ 160 | .coverage 161 | .coverage.* 162 | .cache 163 | nosetests.xml 164 | coverage.xml 165 | *.cover 166 | *.py,cover 167 | .hypothesis/ 168 | .pytest_cache/ 169 | cover/ 170 | 171 | # Translations 172 | *.mo 173 | *.pot 174 | 175 | # Django stuff: 176 | *.log 177 | local_settings.py 178 | db.sqlite3 179 | db.sqlite3-journal 180 | 181 | # Flask stuff: 182 | instance/ 183 | .webassets-cache 184 | 185 | # Scrapy stuff: 186 | .scrapy 187 | 188 | # Sphinx documentation 189 | docs/_build/ 190 | 191 | # PyBuilder 192 | .pybuilder/ 193 | target/ 194 | 195 | # Jupyter Notebook 196 | .ipynb_checkpoints 197 | 198 | # IPython 199 | profile_default/ 200 | ipython_config.py 201 | 202 | # pyenv 203 | # For a library or package, you might want to ignore these files since the code is 204 | # intended to run in multiple environments; otherwise, check them in: 205 | # .python-version 206 | 207 | # pipenv 208 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 209 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 210 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 211 | # install all needed dependencies. 212 | #Pipfile.lock 213 | 214 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 215 | __pypackages__/ 216 | 217 | # Celery stuff 218 | celerybeat-schedule 219 | celerybeat.pid 220 | 221 | # SageMath parsed files 222 | *.sage.py 223 | 224 | # Environments 225 | .env 226 | .venv 227 | env/ 228 | venv/ 229 | ENV/ 230 | env.bak/ 231 | venv.bak/ 232 | 233 | # Spyder project settings 234 | .spyderproject 235 | .spyproject 236 | 237 | # Rope project settings 238 | .ropeproject 239 | 240 | # mkdocs documentation 241 | /site 242 | 243 | # mypy 244 | .mypy_cache/ 245 | .dmypy.json 246 | dmypy.json 247 | 248 | # Pyre type checker 249 | .pyre/ 250 | 251 | # pytype static type analyzer 252 | .pytype/ 253 | 254 | # Cython debug symbols 255 | cython_debug/ 256 | 257 | !/assets/ 258 | !/source/assets/ 259 | /source/assets/ 260 | ./source/assets/ 261 | source/venv 262 | venv -------------------------------------------------------------------------------- /source/controller/web_driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | WebDriver class for the controller. 3 | """ 4 | import os 5 | 6 | from controller.extension_manager import ExtensionManager 7 | from helper.complement import Complement 8 | from service.driver_action import DriverAction 9 | from service.driver_manager import DriverManager 10 | from service.user_agent_browser import UserAgentBrowser 11 | 12 | 13 | class WebDriver: 14 | """ WebDriver class for the controller. """ 15 | path_assets = None 16 | browser = None 17 | environment = 'local' 18 | driver = None 19 | driver_manager = None 20 | driver_action = None 21 | extension_manager = None 22 | 23 | def __init__(self, path_assets, browser, environment): 24 | self.path_assets = path_assets 25 | self.browser = browser 26 | self.environment = environment 27 | self._init() 28 | 29 | def _init(self): 30 | self._start_driver() 31 | 32 | def _get_is_configured(self): 33 | path_file = f"{self.path_assets}/config_{self.browser}" 34 | return Complement.check_file_exist(path_file) 35 | 36 | def _start_driver(self): 37 | if not self._get_is_configured(): 38 | self._configure_driver() 39 | 40 | self._download_extension() 41 | self._init_driver() 42 | 43 | def _configure_driver(self): 44 | self._init_driver_manager() 45 | self._install_driver() 46 | self._init_single_driver() 47 | self._install_user_agent() 48 | self._make_config_file() 49 | self.driver.quit() 50 | 51 | def _init_driver_manager(self): 52 | driver_manager = DriverManager(self.path_assets, self.browser, self.environment) 53 | self.driver_manager = driver_manager 54 | 55 | def _install_driver(self): 56 | self.driver_manager.setup_driver() 57 | 58 | def _check_managers(self): 59 | self.driver = None 60 | if self.driver_manager is None: 61 | self._init_driver_manager() 62 | 63 | def _init_single_driver(self): 64 | self.driver_manager.configure_single() 65 | self.driver = self.driver_manager.get_driver() 66 | 67 | def _init_driver(self): 68 | self._check_managers() 69 | 70 | path_extensions = self._get_path_extension() 71 | self.driver_manager.configure_master(path_extensions) 72 | self.driver = self.driver_manager.get_driver() 73 | self._set_driver_action() 74 | if path_extensions is not None and len(path_extensions) > 0: 75 | self._set_extension_manager() 76 | 77 | def _install_user_agent(self): 78 | user_agent_browser = UserAgentBrowser(self.path_assets, self.browser, self.driver) 79 | if not user_agent_browser.exist_user_agent(): 80 | user_agent_browser.data_user_agent() 81 | 82 | def _make_config_file(self): 83 | Complement.write_file(f"{self.path_assets}/config_{self.browser}", "True") 84 | 85 | def get_driver_manager(self): 86 | """ Return the driver manager. """ 87 | return self.driver_manager 88 | 89 | def get_driver(self): 90 | """ Return the driver. """ 91 | return self.driver 92 | 93 | def _download_extension(self): 94 | self.extension_manager = ExtensionManager(self.path_assets, self.browser) 95 | 96 | def _set_extension_manager(self): 97 | self.extension_manager = None 98 | self.extension_manager = ExtensionManager(self.path_assets, self.browser, 99 | self.driver_action, self.driver) 100 | 101 | def _get_path_extension(self): 102 | if self.extension_manager is not None: 103 | return self.extension_manager.get_extensions() 104 | return [] 105 | 106 | def _set_driver_action(self): 107 | is_chrome = Complement.browser_is_chrome(self.browser) 108 | is_firefox = Complement.browser_is_firefox(self.browser) 109 | self.driver_action = DriverAction(self.driver, is_chrome, is_firefox) 110 | 111 | def start(self): 112 | """ Start the driver """ 113 | self.driver_action.set_setting_window() 114 | 115 | # Example: 116 | self.example_extensions() 117 | 118 | self.driver_action.close_window() 119 | 120 | def example_extensions(self): 121 | """ Example of use of the extension manager. """ 122 | extensions = os.getenv('EXTENSIONS_' + self.browser.upper()) or None 123 | if extensions is None: 124 | # Normal 125 | self.driver_action.wait_time() 126 | print(self.driver_action.get_title()) 127 | else: 128 | self.example_extensions_basic(extensions) 129 | 130 | def example_extensions_basic(self, extensions): 131 | """ Example extensions """ 132 | extensions = Complement.convertENVArray(extensions) 133 | 134 | for extension in extensions: 135 | match extension: 136 | case 'metamask': 137 | # Metamask 138 | self.example_metamask() 139 | case 'captcha': 140 | # CAPTCHA 141 | self.example_captcha() 142 | 143 | def example_metamask(self): 144 | """ Example of metamask """ 145 | keys = os.getenv('METAMASK_AUTH') or "" 146 | keys = Complement.convertENVArray(keys) 147 | self.extension_manager.wallet.set_passwords(keys[0], keys[1]) 148 | self.extension_manager.wallet.login() 149 | 150 | def example_captcha(self): 151 | """ Example of CAPTCHA """ 152 | self.extension_manager.captcha.set_test_url() 153 | self.extension_manager.captcha.resolve() 154 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = false 9 | max_line_length = 120 10 | tab_width = 4 11 | ij_continuation_indent_size = 8 12 | ij_formatter_off_tag = @formatter:off 13 | ij_formatter_on_tag = @formatter:on 14 | ij_formatter_tags_enabled = false 15 | ij_smart_tabs = false 16 | ij_visual_guides = none 17 | ij_wrap_on_typing = false 18 | 19 | [.editorconfig] 20 | ij_editorconfig_align_group_field_declarations = false 21 | ij_editorconfig_space_after_colon = false 22 | ij_editorconfig_space_after_comma = true 23 | ij_editorconfig_space_before_colon = false 24 | ij_editorconfig_space_before_comma = false 25 | ij_editorconfig_spaces_around_assignment_operators = true 26 | 27 | [{*.markdown,*.md}] 28 | ij_markdown_force_one_space_after_blockquote_symbol = true 29 | ij_markdown_force_one_space_after_header_symbol = true 30 | ij_markdown_force_one_space_after_list_bullet = true 31 | ij_markdown_force_one_space_between_words = true 32 | ij_markdown_insert_quote_arrows_on_wrap = true 33 | ij_markdown_keep_indents_on_empty_lines = false 34 | ij_markdown_keep_line_breaks_inside_text_blocks = true 35 | ij_markdown_max_lines_around_block_elements = 1 36 | ij_markdown_max_lines_around_header = 1 37 | ij_markdown_max_lines_between_paragraphs = 1 38 | ij_markdown_min_lines_around_block_elements = 1 39 | ij_markdown_min_lines_around_header = 1 40 | ij_markdown_min_lines_between_paragraphs = 1 41 | ij_markdown_wrap_text_if_long = true 42 | ij_markdown_wrap_text_inside_blockquotes = true 43 | 44 | [{*.py,*.pyw}] 45 | ij_python_align_collections_and_comprehensions = true 46 | ij_python_align_multiline_imports = true 47 | ij_python_align_multiline_parameters = true 48 | ij_python_align_multiline_parameters_in_calls = true 49 | ij_python_blank_line_at_file_end = true 50 | ij_python_blank_lines_after_imports = 1 51 | ij_python_blank_lines_after_local_imports = 0 52 | ij_python_blank_lines_around_class = 1 53 | ij_python_blank_lines_around_method = 1 54 | ij_python_blank_lines_around_top_level_classes_functions = 2 55 | ij_python_blank_lines_before_first_method = 0 56 | ij_python_call_parameters_new_line_after_left_paren = false 57 | ij_python_call_parameters_right_paren_on_new_line = false 58 | ij_python_call_parameters_wrap = normal 59 | ij_python_dict_alignment = 0 60 | ij_python_dict_new_line_after_left_brace = false 61 | ij_python_dict_new_line_before_right_brace = false 62 | ij_python_dict_wrapping = 1 63 | ij_python_from_import_new_line_after_left_parenthesis = false 64 | ij_python_from_import_new_line_before_right_parenthesis = false 65 | ij_python_from_import_parentheses_force_if_multiline = false 66 | ij_python_from_import_trailing_comma_if_multiline = false 67 | ij_python_from_import_wrapping = 1 68 | ij_python_hang_closing_brackets = false 69 | ij_python_keep_blank_lines_in_code = 1 70 | ij_python_keep_blank_lines_in_declarations = 1 71 | ij_python_keep_indents_on_empty_lines = false 72 | ij_python_keep_line_breaks = true 73 | ij_python_method_parameters_new_line_after_left_paren = false 74 | ij_python_method_parameters_right_paren_on_new_line = false 75 | ij_python_method_parameters_wrap = normal 76 | ij_python_new_line_after_colon = false 77 | ij_python_new_line_after_colon_multi_clause = true 78 | ij_python_optimize_imports_always_split_from_imports = false 79 | ij_python_optimize_imports_case_insensitive_order = false 80 | ij_python_optimize_imports_join_from_imports_with_same_source = false 81 | ij_python_optimize_imports_sort_by_type_first = true 82 | ij_python_optimize_imports_sort_imports = true 83 | ij_python_optimize_imports_sort_names_in_from_imports = false 84 | ij_python_space_after_comma = true 85 | ij_python_space_after_number_sign = true 86 | ij_python_space_after_py_colon = true 87 | ij_python_space_before_backslash = true 88 | ij_python_space_before_comma = false 89 | ij_python_space_before_for_semicolon = false 90 | ij_python_space_before_lbracket = false 91 | ij_python_space_before_method_call_parentheses = false 92 | ij_python_space_before_method_parentheses = false 93 | ij_python_space_before_number_sign = true 94 | ij_python_space_before_py_colon = false 95 | ij_python_space_within_empty_method_call_parentheses = false 96 | ij_python_space_within_empty_method_parentheses = false 97 | ij_python_spaces_around_additive_operators = true 98 | ij_python_spaces_around_assignment_operators = true 99 | ij_python_spaces_around_bitwise_operators = true 100 | ij_python_spaces_around_eq_in_keyword_argument = false 101 | ij_python_spaces_around_eq_in_named_parameter = false 102 | ij_python_spaces_around_equality_operators = true 103 | ij_python_spaces_around_multiplicative_operators = true 104 | ij_python_spaces_around_power_operator = true 105 | ij_python_spaces_around_relational_operators = true 106 | ij_python_spaces_around_shift_operators = true 107 | ij_python_spaces_within_braces = false 108 | ij_python_spaces_within_brackets = false 109 | ij_python_spaces_within_method_call_parentheses = false 110 | ij_python_spaces_within_method_parentheses = false 111 | ij_python_use_continuation_indent_for_arguments = false 112 | ij_python_use_continuation_indent_for_collection_and_comprehensions = false 113 | ij_python_use_continuation_indent_for_parameters = true 114 | ij_python_wrap_long_lines = false 115 | 116 | [{*.yaml,*.yml}] 117 | indent_size = 2 118 | ij_yaml_align_values_properties = do_not_align 119 | ij_yaml_autoinsert_sequence_marker = true 120 | ij_yaml_block_mapping_on_new_line = false 121 | ij_yaml_indent_sequence_value = true 122 | ij_yaml_keep_indents_on_empty_lines = false 123 | ij_yaml_keep_line_breaks = true 124 | ij_yaml_sequence_on_new_line = false 125 | ij_yaml_space_before_colon = false 126 | ij_yaml_spaces_within_braces = true 127 | ij_yaml_spaces_within_brackets = true 128 | -------------------------------------------------------------------------------- /source/service/driver/configuration_chrome.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | """ 3 | Configuration for Chrome. 4 | """ 5 | import os 6 | 7 | from selenium import webdriver 8 | from selenium.webdriver.chrome.service import Service as ServiceChrome 9 | 10 | from interface.driver_interface import DriverInterface 11 | 12 | 13 | class ConfigurationChrome(DriverInterface): 14 | """ Configuration Chrome class """ 15 | 16 | def __init__(self, path_driver, path_assets, environment): 17 | super().__init__(path_driver, path_assets, environment) 18 | 19 | def set_driver_extension(self, path_extensions): 20 | """ Set driver extension """ 21 | options_browser = self._get_options() 22 | options_browser = self._set_extension_pre_config(options_browser, path_extensions) 23 | self.driver = self._get_manager_driver(options_browser) 24 | 25 | def _get_extension_pre_config(self, options_browser): 26 | """ Get extension pre config """ 27 | options_browser.accept_untrusted_certs = True 28 | options_browser.accept_insecure_certs = True 29 | # options_browser.add_argument('--disable-popup-blocking') 30 | 31 | return options_browser 32 | 33 | def _set_extension_pre_config(self, options_browser, path_extensions): 34 | """ Set extension pre config """ 35 | options_browser = self._get_extension_pre_config(options_browser) 36 | for path_extension in path_extensions: 37 | options_browser = self._set_manager_extension_pre_config(options_browser, path_extension) 38 | return options_browser 39 | 40 | def _set_manager_extension_pre_config(self, options_browser, path_extension): 41 | """ Set manager extension pre config """ 42 | match self.environment: 43 | case 'local': 44 | return self._set_extension_pre_local(options_browser, path_extension) 45 | case 'docker': 46 | return self._set_extension_pre_docker(options_browser, path_extension) 47 | case 'remote': 48 | return self._set_extension_pre_remote(options_browser, path_extension) 49 | case default: 50 | return options_browser 51 | 52 | def _set_extension_pre_local(self, options_browser, path_extension): 53 | """ Set extension pre local """ 54 | # options_browser.add_argument(f'--load-extension = {extension}') 55 | options_browser.add_extension(path_extension) 56 | return options_browser 57 | 58 | def _set_extension_pre_docker(self, options_browser, path_extension): 59 | """ Set extension pre docker """ 60 | # options_browser.add_argument(f'--load-extension = {extension}') 61 | options_browser.add_extension(path_extension) 62 | return options_browser 63 | 64 | def _set_extension_pre_remote(self, options_browser, path_extension): 65 | """ Set extension pre remote """ 66 | # options_browser.add_argument(f'--load-extension = {extension}') 67 | options_browser.add_extension(path_extension) 68 | return options_browser 69 | 70 | def _get_driver_local(self, options_browser): 71 | """ Get driver local """ 72 | service = self._get_service() 73 | return webdriver.Chrome(service=service, options=options_browser) 74 | 75 | def _get_driver_docker(self, options_browser): 76 | """ Get driver docker """ 77 | return webdriver.Chrome(options=options_browser) 78 | 79 | def _get_service(self): 80 | """ Get service """ 81 | service_browser = ServiceChrome(self.path_driver) 82 | return service_browser 83 | 84 | def _set_service_option(self): 85 | """ Set service option """ 86 | return webdriver.ChromeOptions() 87 | 88 | def _get_options_general(self, options_browser): 89 | """ Get options general """ 90 | options_browser.add_argument("--lang=en-US") 91 | options_browser.add_argument("--disable-infobars") 92 | options_browser.add_argument("--start-maximized") 93 | options_browser.add_experimental_option( 94 | "prefs", 95 | { 96 | "intl.accept_languages": "en,en_US", 97 | "download.default_directory": os.getcwd(), 98 | "download.prompt_for_download": False, 99 | }, 100 | ) 101 | options_browser.add_experimental_option("excludeSwitches", ["enable-logging"]) 102 | options_browser.add_experimental_option("excludeSwitches", ["enable-automation"]) 103 | options_browser.add_experimental_option("useAutomationExtension", False) 104 | return options_browser 105 | 106 | def _get_options_local(self, options_browser): 107 | """ Get options local """ 108 | # options_browser.add_argument("--no-sandbox") 109 | options_browser.add_argument("--log-level=3") 110 | options_browser.add_argument("--disable-gpu") 111 | options_browser.add_argument("--no-first-run") 112 | options_browser.add_argument("--disk-cache-size=1") 113 | options_browser.add_argument("--media-cache-size=1") 114 | options_browser.add_argument("--no-service-autorun") 115 | options_browser.add_argument("--password-store=basic") 116 | # options_browser.add_argument("--disable-dev-shm-usage") 117 | options_browser.add_argument("--disable-application-cache") 118 | options_browser.add_argument("--disable-blink-features=AutomationControlled") 119 | return options_browser 120 | 121 | def _get_options_docker(self, options_browser): 122 | """ Get options docker """ 123 | # options_browser.add_argument('--headless') 124 | options_browser.add_argument('--no-sandbox') 125 | options_browser.add_argument('--disable-dev-shm-usage') 126 | return options_browser 127 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ### VisualStudioCode template 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | *.code-workspace 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | ### JetBrains template 13 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 14 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 15 | 16 | # User-specific stuff 17 | .idea/**/workspace.xml 18 | .idea/**/tasks.xml 19 | .idea/**/usage.statistics.xml 20 | .idea/**/dictionaries 21 | .idea/**/shelf 22 | 23 | # Generated files 24 | .idea/**/contentModel.xml 25 | 26 | # Sensitive or high-churn files 27 | .idea/**/dataSources/ 28 | .idea/**/dataSources.ids 29 | .idea/**/dataSources.local.xml 30 | .idea/**/sqlDataSources.xml 31 | .idea/**/dynamic.xml 32 | .idea/**/uiDesigner.xml 33 | .idea/**/dbnavigator.xml 34 | 35 | # Gradle 36 | .idea/**/gradle.xml 37 | .idea/**/libraries 38 | 39 | # Gradle and Maven with auto-import 40 | # When using Gradle or Maven with auto-import, you should exclude module files, 41 | # since they will be recreated, and may cause churn. Uncomment if using 42 | # auto-import. 43 | # .idea/artifacts 44 | # .idea/compiler.xml 45 | # .idea/jarRepositories.xml 46 | # .idea/modules.xml 47 | # .idea/*.iml 48 | # .idea/modules 49 | # *.iml 50 | # *.ipr 51 | 52 | # CMake 53 | cmake-build-*/ 54 | 55 | # Mongo Explorer plugin 56 | .idea/**/mongoSettings.xml 57 | 58 | # File-based project format 59 | *.iws 60 | 61 | # IntelliJ 62 | out/ 63 | 64 | # mpeltonen/sbt-idea plugin 65 | .idea_modules/ 66 | 67 | # JIRA plugin 68 | atlassian-ide-plugin.xml 69 | 70 | # Cursive Clojure plugin 71 | .idea/replstate.xml 72 | 73 | # Crashlytics plugin (for Android Studio and IntelliJ) 74 | com_crashlytics_export_strings.xml 75 | crashlytics.properties 76 | crashlytics-build.properties 77 | fabric.properties 78 | 79 | # Editor-based Rest Client 80 | .idea/httpRequests 81 | 82 | # Android studio 3.1+ serialized cache file 83 | .idea/caches/build_file_checksums.ser 84 | 85 | ### VirtualEnv template 86 | # Virtualenv 87 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 88 | .Python 89 | [Bb]in 90 | [Ii]nclude 91 | [Ll]ib 92 | [Ll]ib64 93 | [Ll]ocal 94 | [Ss]cripts 95 | pyvenv.cfg 96 | .venv 97 | pip-selfcheck.json 98 | 99 | ### Vim template 100 | # Swap 101 | [._]*.s[a-v][a-z] 102 | !*.svg # comment out if you don't need vector files 103 | [._]*.sw[a-p] 104 | [._]s[a-rt-v][a-z] 105 | [._]ss[a-gi-z] 106 | [._]sw[a-p] 107 | 108 | # Session 109 | Session.vim 110 | Sessionx.vim 111 | 112 | # Temporary 113 | .netrwhist 114 | *~ 115 | # Auto-generated tag files 116 | tags 117 | # Persistent undo 118 | [._]*.un~ 119 | 120 | ### macOS template 121 | # General 122 | .DS_Store 123 | .AppleDouble 124 | .LSOverride 125 | 126 | # Icon must end with two \r 127 | Icon 128 | 129 | # Thumbnails 130 | ._* 131 | 132 | # Files that might appear in the root of a volume 133 | .DocumentRevisions-V100 134 | .fseventsd 135 | .Spotlight-V100 136 | .TemporaryItems 137 | .Trashes 138 | .VolumeIcon.icns 139 | .com.apple.timemachine.donotpresent 140 | 141 | # Directories potentially created on remote AFP share 142 | .AppleDB 143 | .AppleDesktop 144 | Network Trash Folder 145 | Temporary Items 146 | .apdisk 147 | 148 | ### Python template 149 | # Byte-compiled / optimized / DLL files 150 | __pycache__/ 151 | *.py[cod] 152 | *$py.class 153 | 154 | # C extensions 155 | *.so 156 | 157 | # Distribution / packaging 158 | .Python 159 | build/ 160 | develop-eggs/ 161 | dist/ 162 | downloads/ 163 | eggs/ 164 | .eggs/ 165 | lib/ 166 | lib64/ 167 | parts/ 168 | sdist/ 169 | var/ 170 | wheels/ 171 | share/python-wheels/ 172 | *.egg-info/ 173 | .installed.cfg 174 | *.egg 175 | MANIFEST 176 | 177 | # PyInstaller 178 | # Usually these files are written by a python script from a template 179 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 180 | *.manifest 181 | *.spec 182 | 183 | # Installer logs 184 | pip-log.txt 185 | pip-delete-this-directory.txt 186 | 187 | # Unit test / coverage reports 188 | htmlcov/ 189 | .tox/ 190 | .nox/ 191 | .coverage 192 | .coverage.* 193 | .cache 194 | nosetests.xml 195 | coverage.xml 196 | *.cover 197 | *.py,cover 198 | .hypothesis/ 199 | .pytest_cache/ 200 | cover/ 201 | 202 | # Translations 203 | *.mo 204 | *.pot 205 | 206 | # Django stuff: 207 | *.log 208 | local_settings.py 209 | db.sqlite3 210 | db.sqlite3-journal 211 | 212 | # Flask stuff: 213 | instance/ 214 | .webassets-cache 215 | 216 | # Scrapy stuff: 217 | .scrapy 218 | 219 | # Sphinx documentation 220 | docs/_build/ 221 | 222 | # PyBuilder 223 | .pybuilder/ 224 | target/ 225 | 226 | # Jupyter Notebook 227 | .ipynb_checkpoints 228 | 229 | # IPython 230 | profile_default/ 231 | ipython_config.py 232 | 233 | # pyenv 234 | # For a library or package, you might want to ignore these files since the code is 235 | # intended to run in multiple environments; otherwise, check them in: 236 | # .python-version 237 | 238 | # pipenv 239 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 240 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 241 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 242 | # install all needed dependencies. 243 | #Pipfile.lock 244 | 245 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 246 | __pypackages__/ 247 | 248 | # Celery stuff 249 | celerybeat-schedule 250 | celerybeat.pid 251 | 252 | # SageMath parsed files 253 | *.sage.py 254 | 255 | # Environments 256 | .venv 257 | env/ 258 | venv/ 259 | ENV/ 260 | env.bak/ 261 | venv.bak/ 262 | 263 | # Spyder project settings 264 | .spyderproject 265 | .spyproject 266 | 267 | # Rope project settings 268 | .ropeproject 269 | 270 | # mkdocs documentation 271 | /site 272 | 273 | # mypy 274 | .mypy_cache/ 275 | .dmypy.json 276 | dmypy.json 277 | 278 | # Pyre type checker 279 | .pyre/ 280 | 281 | # pytype static type analyzer 282 | .pytype/ 283 | 284 | # Cython debug symbols 285 | cython_debug/ 286 | 287 | -------------------------------------------------------------------------------- /source/controller/extension/captchasolver.py: -------------------------------------------------------------------------------- 1 | """ 2 | CAPTCHA solver for the extension. 3 | """ 4 | import random 5 | import time 6 | 7 | from interface.extension.plugin_manager import PluginManager 8 | 9 | 10 | def sleep_seconds(): 11 | """Sleep seconds""" 12 | min_rand = 0.64 13 | max_rand = 1.27 14 | time.sleep(random.uniform(min_rand, max_rand)) 15 | 16 | 17 | class CaptchaSolver(PluginManager): 18 | """ CAPTCHA solver for the extension. """ 19 | 20 | # Variables 21 | solver_challenge = False 22 | # Settings 23 | RECAPTCHA_PAGE_URL = "https://patrickhlauke.github.io/recaptcha" 24 | # RECAPTCHA_PAGE_URL = "https://www.google.com/recaptcha/api2/demo" 25 | # Chrome 26 | url_extension = ( 27 | "https://chrome.google.com/webstore/detail/" 28 | "buster-captcha-solver-for/" 29 | "mpbjkejclgfgadiemmefgebjfooflfhl" 30 | ) 31 | # Firefox 32 | data_extension_firefox = { 33 | "user_id": [12929064], 34 | "url": "https://addons.mozilla.org/en-US/firefox/" 35 | "addon/buster-captcha-solver", 36 | "collection_url": "https://addons.mozilla.org/en-US/" 37 | "firefox/addon/youtube-video-quality", 38 | } 39 | 40 | def __init__(self, path_assets, browser="chrome", 41 | driver_action=None, driver=None): 42 | self.path_assets = path_assets 43 | self.browser = browser 44 | self.driver_action = driver_action 45 | self.driver = driver 46 | 47 | self._init_variables() 48 | 49 | super().__init__(path_assets, browser, driver) 50 | 51 | def _init_variables(self): 52 | self.download_extension = None 53 | self.path_data = f"{self.path_assets}/{self.path_data}" 54 | 55 | def set_test_url(self): 56 | """Set the test url""" 57 | self.driver_action.set_setting_window(self.RECAPTCHA_PAGE_URL) 58 | sleep_seconds() 59 | 60 | def resolve(self): 61 | """Resolve the captcha Google""" 62 | if self._check_webdriver(): 63 | self._resolve_captcha() 64 | 65 | def _check_webdriver(self): 66 | if self.driver_action is None or self.driver is None: 67 | return False 68 | if self.driver_action and self.driver: 69 | return True 70 | return False 71 | 72 | def _resolve_captcha(self): 73 | self._start_tries() 74 | 75 | def _start_tries(self): 76 | counter = 0 77 | number_of_iterations = 10 78 | for i in range(number_of_iterations): 79 | if self.solver_challenge: 80 | break 81 | 82 | if self._check_exist_captcha(): 83 | self._solve() 84 | counter += 1 85 | else: 86 | break 87 | print(f"Successful breaks: {counter} - {i}") 88 | 89 | print(f"Total successful breaks: {counter}") 90 | 91 | def _check_exist_captcha(self): 92 | return self.driver_action.visible("//iframe[contains(@src,'recaptcha')]") or False 93 | 94 | def _solve(self): 95 | # Get a ReCaptcha Challenge 96 | if self._get_recaptcha_challenge(): 97 | # Switch to page's main frame 98 | self.driver_action.switch_to_default_content() 99 | # Get audio challenge 100 | check_challenge = self._get_audio_challenge() 101 | if check_challenge: 102 | self.solver_challenge = check_challenge 103 | # Switch to the ReCaptcha iframe to verify it is solved 104 | self.driver_action.switch_to_default_content() 105 | 106 | def _get_recaptcha_challenge(self): 107 | iframes = self._get_all_iframes() 108 | if self._switch_recaptcha_iframe(iframes): 109 | self._click_recaptcha_checkbox() 110 | return self._check_recaptcha_has_challenge() 111 | return False 112 | 113 | def _get_all_iframes(self): 114 | iframes = self.driver_action.find_all_by_tag('iframe') 115 | if len(iframes) > 0: 116 | return iframes 117 | return [] 118 | 119 | def _switch_recaptcha_iframe(self, iframes): 120 | # Switch focus to ReCaptcha iframe 121 | self.driver_action.switch_to_iframe(iframes[0]) 122 | check_exist = self.driver_action.is_exists_by_xpath( 123 | '//div[@class="recaptcha-checkbox-checkmark" and @role="presentation"]') 124 | if not check_exist: 125 | print("No element in the frame!!") 126 | return check_exist 127 | 128 | def _click_recaptcha_checkbox(self): 129 | xpath = '//div[@class="recaptcha-checkbox-border"]' 130 | if self.driver_action.is_exists_by_xpath(xpath): 131 | self.driver_action.clickable(xpath) 132 | 133 | def _check_recaptcha_has_challenge(self): 134 | # Check if the ReCaptcha has no challenge 135 | exist = self.driver_action.is_exists_by_xpath('//span[@aria-checked="true"]') 136 | if exist: 137 | print("ReCaptcha has no challenge. Trying again!") 138 | return not exist 139 | 140 | def _get_audio_challenge(self): 141 | # Get all the iframes on the page again 142 | iframes = self._get_all_iframes() 143 | # Switch to the last iframe (the new one) 144 | self.driver_action.switch_to_iframe(iframes[-1]) 145 | # Check and press Audio challenge button 146 | if self._check_audio_button(): 147 | if not self._check_submit_button(): 148 | return True 149 | 150 | return self._check_recaptcha_audio_button() 151 | 152 | def _check_recaptcha_audio_button(self): 153 | if not self._check_button('//button[@id="recaptcha-audio-button"]'): 154 | print("No element of audio challenge!!") 155 | return False 156 | return True 157 | 158 | def _check_audio_button(self): 159 | # Check if the audio challenge button is present 160 | rule_first = "contains(@class, 'button-holder')" 161 | rule_second = "contains(@class, 'help-button-holder')" 162 | xpath = f"//div[{rule_first} and {rule_second}]" 163 | # Check Audio Button 164 | check_exist = self._check_button(xpath) 165 | if check_exist: 166 | # Click on the Audio Button 167 | self._click_button(xpath) 168 | return check_exist 169 | 170 | def _check_submit_button(self): 171 | # Check again button is present 172 | rule_first = "contains(@class, 'rc-button-default')" 173 | rule_second = "contains(@class, 'goog-inline-block')" 174 | xpath = f"//*[{rule_first} and {rule_second}]" 175 | # Check Audio Button 176 | check_exist = self._check_button(xpath) 177 | if check_exist: 178 | # Click on the Audio Button 179 | self._click_button(xpath) 180 | return check_exist 181 | 182 | def _check_button(self, xpath): 183 | check_exist = self.driver_action.is_exists_by_xpath(xpath) 184 | if not check_exist: 185 | print("No exist button") 186 | return check_exist 187 | 188 | def _click_button(self, xpath): 189 | self.driver_action.clickable(xpath) 190 | -------------------------------------------------------------------------------- /source/controller/extension/metamask.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | """ 3 | Metamask extension for the controller. 4 | """ 5 | 6 | from interface.extension.plugin_manager import PluginManager 7 | 8 | 9 | class MetaMask(PluginManager): 10 | """ Metamask extension for the controller. """ 11 | path_data = "wallets/metamask" 12 | # Data 13 | recovery_phrase = "" 14 | password = "" 15 | 16 | # Chrome 17 | url_extension = ( 18 | "https://chrome.google.com/webstore/detail/" 19 | "metamask/nkbihfbeogaeaoehlefnkodbefgpgknn" 20 | ) 21 | # Firefox 22 | data_extension_firefox = { 23 | "url": "https://addons.mozilla.org/en-US/firefox/addon/ether-metamask", 24 | "collection_url": "https://addons.mozilla.org/en-US/firefox/" 25 | "addon/metamask-legacy-web3/", 26 | "user_id": [12436990, 13014139], 27 | } 28 | 29 | def __init__(self, path_assets, browser="chrome", 30 | driver_action=None, driver=None): 31 | self.path_assets = path_assets 32 | self.browser = browser 33 | 34 | self.driver_action = driver_action 35 | self.driver = driver 36 | 37 | self._init_variables() 38 | 39 | super().__init__(path_assets, browser, driver) 40 | 41 | def _init_variables(self): 42 | self.fails = 0 43 | self.private_key = None 44 | self.password = None 45 | self.recovery_phrase = None 46 | self.download_extension = None 47 | self.path_data = f"{self.path_assets}/{self.path_data}" 48 | 49 | # Init Process 50 | def set_passwords(self, password: str = None, recovery_phrase: str = None): 51 | """Set the password and the recovery phrase.""" 52 | # question = "What is your MetaMask password?" 53 | self.password = password 54 | # question = "What is your MetaMask recovery phrase?" 55 | self.recovery_phrase = recovery_phrase 56 | 57 | # Selenium Process 58 | 59 | def login(self): 60 | """Login to the MetaMask extension.""" 61 | try: 62 | # Switch to the MetaMask extension tab. 63 | self.driver_action.window_handles(0) 64 | # Prevent a blank page. 65 | self.driver.refresh() 66 | # Click on the "Start" button. 67 | self.driver_action.clickable('//*[@class="welcome-page"]/button') 68 | # Click on the "Import wallet" button. 69 | self.driver_action.clickable('//*[contains(@class, "btn-primary")][position()=1]') 70 | # Click on the "I agree" button. 71 | self.driver_action.clickable('//footer/button[2]') 72 | # Put the password 73 | self._set_data_password() 74 | # Wait until the login worked and click on the "All done" button. 75 | self.driver_action.visible('//*[contains(@class, "emoji")][position()=1]') 76 | # Confirm the connection to MetaMask. 77 | self.driver_action.clickable('//*[contains(@class, "btn-primary")][position()=1]') 78 | # Set Private Key 79 | self._set_private_key() 80 | except (Exception,): # Failed - a web element is not accessible. 81 | self.fails += 1 # Increment the counter. 82 | if self.fails < 2: # Retry login to the MetaMask. 83 | self.login() 84 | else: # Failed twice - the wallet is not accessible. 85 | self.driver.quit() # Stop the webdriver. 86 | 87 | def _set_data_password(self): 88 | # If a recovery phrase and password are set. 89 | if self.recovery_phrase != '' and self.password != '': 90 | # Count the words in the recovery phrase. 91 | split_recovery_phrase = self.recovery_phrase.split(" ") 92 | count = len(split_recovery_phrase) 93 | # Select by length the recovery phrase. 94 | xpath = '//select[contains(@class, "dropdown__select")]' 95 | self.driver_action.select_by_value(xpath, count) 96 | # Put the recovery phrase. 97 | for i in range(count): 98 | # Get the word. 99 | word = split_recovery_phrase[i] 100 | # Set the word. 101 | self.driver_action.send_keys(f'//*[@id="import-srp__srp-word-{i}"]', word) 102 | # Input a password of your account. 103 | for path in ('//*[@id="password"]', '//*[@id="confirm-password"]'): 104 | self.driver_action.send_keys(path, self.password) 105 | # Click on the "I have read and agree to the..." checkbox. 106 | self.driver_action.clickable('(//*[@id="create-new-vault__terms-checkbox"])[1]') 107 | # Click on the "Import" button. 108 | self.driver_action.clickable('//*[contains(@class, "btn-primary")][position()=1]') 109 | 110 | def _set_private_key(self): 111 | if self.private_key is not None: # Change account. 112 | self.driver_action.clickable('//button[@data-testid="popover-close"]') 113 | self.driver_action.clickable( # Click on the menu icon. 114 | '//*[@class="account-menu__icon"][position()=1]') 115 | self.driver_action.clickable('//*[contains(@class, "account-menu__item--' 116 | 'clickable")][position()=2]') 117 | self.driver_action.send_keys( # Input the private key. 118 | '//*[@id="private-key-box"]', self.private_key) 119 | self.driver_action.clickable('//*[contains(@class, "btn-secondary")]' 120 | '[position()=1]') 121 | 122 | def sign(self, contract: bool = True, page: int = 2) -> None: 123 | """Sign the MetaMask contract to login to OpenSea.""" 124 | windows = self.driver_action.driver.window_handles # Opened windows. 125 | for _ in range(page): # "Next" and "Connect" buttons. 126 | self.driver_action.window_handles(2) # Switch to the MetaMask pop up tab. 127 | self.driver_action.clickable('//*[contains(@class, "btn-primary")]') 128 | if contract: 129 | self.driver_action.wait_new_tab(windows) 130 | self.contract() # Sign the contract. 131 | 132 | def contract(self, new_contract: bool = False) -> None: 133 | """Sign a MetaMask contract to upload or confirm sale.""" 134 | self.driver_action.window_handles(2) # Switch to the MetaMask pop up tab. 135 | if self.driver_action.window == 1 and new_contract: # GeckoDriver. 136 | self.driver_action.clickable('(//div[contains(@class, "signature") and ' 137 | 'contains(@class, "scroll")])[position()=1]') 138 | self.driver_action.driver.execute_script( # Scroll down. 139 | 'window.scrollTo(0, document.body.scrollHeight);') 140 | # Click on the "Sign" button - Make a contract link. 141 | rule_first = 'contains(@class, "signature")' 142 | rule_second = 'contains(@class, "footer")' 143 | xpath = f'(//div[{rule_first} and {rule_second}])[position()=1]/button[2]' 144 | self.driver_action.clickable(xpath) 145 | if not self.driver_action.wait_popup_close(): 146 | # Sign the contract a second time. 147 | self.contract() 148 | # Switch back to the OpenSea tab. 149 | self.driver_action.window_handles(1) 150 | 151 | def close(self) -> None: 152 | """Close the MetaMask popup.""" 153 | if len(self.driver_action.driver.window_handles) > 2: 154 | try: 155 | self.driver_action.window_handles(2) # Switch to the MetaMask popup. 156 | self.driver.close() # Close the popup extension. 157 | self.driver_action.window_handles(1) # Switch back to OpenSea. 158 | except (Exception,): 159 | pass # Ignore the exception. 160 | -------------------------------------------------------------------------------- /source/service/driver_action.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | """ 3 | Driver Action Service 4 | """ 5 | import os 6 | import time 7 | from datetime import datetime as dt 8 | 9 | from selenium.common.exceptions import TimeoutException as TE 10 | from selenium.webdriver.common.by import By 11 | from selenium.webdriver.common.keys import Keys 12 | from selenium.webdriver.support import expected_conditions as EC 13 | from selenium.webdriver.support.select import Select 14 | from selenium.webdriver.support.ui import WebDriverWait as WDW 15 | 16 | 17 | class DriverAction: 18 | """ This class is used to perform actions on the webdriver. """ 19 | driver = None 20 | is_chrome = False 21 | is_firefox = False 22 | 23 | def __init__(self, driver, is_chrome, is_firefox): 24 | self.driver = driver 25 | self.is_chrome = is_chrome 26 | self.is_firefox = is_firefox 27 | 28 | def wait(self, seconds: int = 5) -> None: 29 | """ Wait for a certain amount of seconds. """ 30 | try: 31 | WDW(self.driver, seconds).until(lambda _: True) 32 | except Exception as execption: 33 | print("error", execption) 34 | 35 | def set_setting_window(self, start_url="https://google.com"): 36 | """ Start the webdriver. """ 37 | self.driver.get(start_url) 38 | # How many tabs 39 | handles = self.driver.window_handles 40 | size = len(handles) 41 | # Close other tabs if exist (Install extension) 42 | if size > 1: 43 | self.driver.close() 44 | 45 | def get_title(self): 46 | """ Get the title of the web page. """ 47 | return self.driver.title if self.driver.title else "" 48 | 49 | def close_window(self) -> None: 50 | """ Try to close the webdriver. """ 51 | try: 52 | self.driver.quit() 53 | except Exception as execption: 54 | print("error", execption) 55 | 56 | def is_exists_by_xpath(self, xpath, show_error=False): 57 | """ Check if an element exists by xpath. """ 58 | try: 59 | # WDW(self.driver, 5).until(EC.presence_of_element_located((By.XPATH, xpath))) 60 | self.driver.find_element(By.XPATH, xpath) 61 | except Exception as execption: 62 | if show_error: 63 | print("error", execption) 64 | return False 65 | return True 66 | 67 | def switch_to_main_window(self): 68 | """Switch to the main tab.""" 69 | self.window_handles(0) 70 | 71 | def switch_to_popup_window(self): 72 | """ Switch to the MetaMask pop up tab. """ 73 | self.window_handles(1) 74 | 75 | def switch_to_window(self, index: int): 76 | """ Switch to the MetaMask pop up tab. """ 77 | self.window_handles(index) 78 | 79 | def switch_to_iframe(self, iframe): 80 | """ Switch to the MetaMask pop up tab. """ 81 | self.driver.switch_to.frame(iframe) 82 | 83 | def switch_to_default_content(self): 84 | """ Switch to the MetaMask pop up tab. """ 85 | self.driver.switch_to.default_content() 86 | 87 | def check_diff_current_vs_url(self, url): 88 | """ Check if the current url is the same as the url. """ 89 | try: 90 | return WDW(self.driver, 5).until(lambda _: self.driver.current_url != url) 91 | except TE: 92 | print("Timeout while waiting for the upload page.") 93 | return False 94 | except Exception as execption: 95 | print("error", execption) 96 | return False 97 | 98 | def quit(self) -> None: 99 | """Stop the webdriver.""" 100 | try: 101 | self.driver.quit() 102 | except Exception as execption: 103 | print("error", execption) 104 | 105 | def clickable(self, element: str, show_error=False) -> None: 106 | """Click on an element if it's clickable using Selenium.""" 107 | try: 108 | WDW(self.driver, 5).until( 109 | EC.element_to_be_clickable((By.XPATH, element)) 110 | ).click() 111 | except Exception as execption: 112 | if show_error: 113 | print("error", execption) 114 | self.clickable_js(element, show_error) 115 | 116 | def clickable_js(self, element: str, show_error=False) -> None: 117 | """ Click on an element if it's clickable using JavaScript.""" 118 | try: 119 | # JavaScript can bypass this. 120 | self.driver.execute_script("arguments[0].click();", self.visible(element)) 121 | except Exception as execption: 122 | if show_error: 123 | print("error", execption) 124 | 125 | def visible(self, element: str): 126 | """ Check if an element is visible using Selenium. """ 127 | try: 128 | return WDW(self.driver, 15).until( 129 | EC.visibility_of_element_located((By.XPATH, element)) 130 | ) 131 | except Exception as execption: 132 | print("error", execption) 133 | return False 134 | 135 | def find_by_tag(self, element: str): 136 | """ Find an element by tag name. """ 137 | try: 138 | return self.driver.find_element(By.TAG_NAME, element) 139 | except Exception as execption: 140 | print("error", execption) 141 | return [] 142 | 143 | def find_all_by_tag(self, element: str): 144 | """ Find all elements by tag name. """ 145 | try: 146 | return WDW(self.driver, 15).until( 147 | lambda _: self.driver.find_elements(By.TAG_NAME, element) 148 | ) 149 | except Exception as execption: 150 | print("error", execption) 151 | return [] 152 | 153 | def send_keys(self, element: str, keys: str) -> None: 154 | """ Send keys to an element if it's visible using Selenium. """ 155 | try: 156 | self.visible(element).send_keys(keys) 157 | except Exception as execption: 158 | print("error", execption) 159 | WDW(self.driver, 5).until( 160 | EC.presence_of_element_located((By.XPATH, element)) 161 | ).send_keys(keys) 162 | 163 | def send_date(self, element: str, keys: str) -> None: 164 | """ Send a date (DD-MM-YYYY HH:MM) to a date input by clicking on it. """ 165 | # GeckoDriver (Mozilla Firefox). 166 | if self.is_firefox: 167 | self.send_keys(element, '-'.join(reversed(keys.split('-'))) if '-' in keys else keys) 168 | # ChromeDriver (Google Chrome). 169 | if self.is_chrome: 170 | keys = keys.split('-') if '-' in keys else [keys] 171 | keys = [keys[1], keys[0], keys[2]] if len(keys) > 1 else keys 172 | for part in range(len(keys) - 1 if keys[len(keys) - 1] == str( 173 | dt.now().year) else len(keys)): # Number of clicks. 174 | self.clickable(element) # Click first on the element. 175 | self.send_keys(element, keys[part]) # Then send it the date. 176 | 177 | def clear_text(self, element, webdriver) -> None: 178 | """ Clear text from an input. """ 179 | self.clickable(element) # Click on the element then clear its text. 180 | # Note: change with 'darwin' if it's not working on MacOS. 181 | control = Keys.COMMAND if os.name == "posix" else Keys.CONTROL 182 | # ChromeDriver (Google Chrome). 183 | if self.is_chrome: 184 | webdriver.ActionChains(self.driver) \ 185 | .key_down(control) \ 186 | .send_keys('a') \ 187 | .key_up(control) \ 188 | .perform() 189 | # GeckoDriver (Mozilla Firefox). 190 | if self.is_firefox: 191 | self.send_keys(element, (control, 'a')) 192 | 193 | def is_empty(self, element: str, data: str, value: str = '') -> bool: 194 | """ Check if data is empty and input its value. """ 195 | if data != value: # Check if the data is not an empty string 196 | self.send_keys(element, data) # or a default value, and send it. 197 | return False 198 | return True 199 | 200 | def wait_new_tab(self, windows): 201 | """ Wait for the new tab. """ 202 | WDW(self.driver, 10).until( 203 | lambda _: windows != self.driver.window_handles) 204 | 205 | def wait_popup_close(self): 206 | """ Wait for the popup to close. """ 207 | try: 208 | # Wait until the popup is closed. 209 | WDW(self.driver, 10).until(EC.number_of_windows_to_be(2)) 210 | return True 211 | except TE: 212 | return False 213 | except Exception as execption: 214 | print("error", execption) 215 | return False 216 | 217 | def window_handles(self, window_number: int) -> None: 218 | """ Check for window handles and wait until a specific tab is opened. """ 219 | WDW(self.driver, 15).until( 220 | lambda _: len(self.driver.window_handles) > window_number 221 | ) 222 | # Switch to the asked tab. 223 | self.driver.switch_to.window(self.driver.window_handles[window_number]) 224 | 225 | def select_by_value(self, xpath: str, value: str) -> None: 226 | """ Select an option by its value. """ 227 | try: 228 | # Selenium