From d99c93c2959c54f5d86f57d74e791297138ea36a Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Wed, 23 Dec 2020 18:12:32 -0500 Subject: [PATCH 001/150] Update amazon.py added exception handling when starting discord_presence. --- stores/amazon.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index 98bdf944..4cc327a9 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -24,6 +24,8 @@ from utils.logger import log from utils.selenium_utils import options, enable_headless +from pypresence import exceptions as pyexceptions + AMAZON_URLS = { "BASE_URL": "https://{domain}/", "OFFER_URL": "https://{domain}/gp/offer-listing/", @@ -216,7 +218,11 @@ def __init__( self.no_image = no_image presence.enabled = not disable_presence - presence.start_presence() + try: + presence.start_presence() + except Exception in pyexceptions: + log.error("Discord presence failed to load") + presence.enabled = False # Create necessary sub-directories if they don't exist if not os.path.exists("screenshots"): From ca291dd2a0f49ca3208dca6078e9878af2a72e94 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 24 Dec 2020 13:41:12 -0500 Subject: [PATCH 002/150] - Basic global configuration functionality - Initializing global features in cli.py main() - Migrated Amazon strings to global configuration file - Introduced common package - Migrated to Version class for version information - Added configured Amazon shopping Domain to Amazon config printout - Trimmed console logging to allow more useful console space --- cli/cli.py | 29 +++---- common/__init__.py | 0 common/config.py | 48 ++++++++++ config/fairgame.conf | 110 +++++++++++++++++++++++ stores/amazon.py | 203 ++++++++----------------------------------- utils/logger.py | 2 +- utils/version.py | 46 +++++++--- 7 files changed, 240 insertions(+), 198 deletions(-) create mode 100644 common/__init__.py create mode 100644 common/config.py create mode 100644 config/fairgame.conf diff --git a/cli/cli.py b/cli/cli.py index ce89f7a6..3d7bc212 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -1,23 +1,17 @@ +import time +import click from datetime import datetime from functools import wraps from signal import signal, SIGINT - -import click -import time - from notifications.notifications import NotificationHandler, TIME_FORMAT -from stores.amazon import Amazon -from stores.bestbuy import BestBuyHandler -from utils import selenium_utils from utils.logger import log +from common.config import Config from utils.version import check_version +from stores.amazon import Amazon +from stores.bestbuy import BestBuyHandler -notification_handler = NotificationHandler() - -try: - check_version() -except Exception as e: - log.error(e) +global_config = None +notification_handler = None def handler(signal, frame): @@ -41,6 +35,12 @@ def decorator(*args, **kwargs): @click.group() def main(): + global global_config + global notification_handler + # Global scope stuff here + check_version() + global_config = Config() + notification_handler = NotificationHandler() pass @@ -142,7 +142,6 @@ def amazon( slow_mode, p, ): - notification_handler.sound_enabled = not disable_sound if not notification_handler.sound_enabled: log.info("Local sounds have been disabled.") @@ -157,8 +156,8 @@ def amazon( no_screenshots=no_screenshots, disable_presence=disable_presence, slow_mode=slow_mode, - encryption_pass=p, no_image=no_image, + encryption_pass=p, ) try: amzn_obj.run(delay=delay, test=test) diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/common/config.py b/common/config.py new file mode 100644 index 00000000..ff497549 --- /dev/null +++ b/common/config.py @@ -0,0 +1,48 @@ +import os +import config +import stdiomask + +from utils.encryption import load_encrypted_config, create_encrypted_config +from utils.logger import log + +GLOBAL_CONFIG_FILE = "config/fairgame.conf" +AMAZON_CREDENTIAL_FILE = "config/amazon_credentials.json" + + +def await_credential_input(): + username = input("Amazon login ID: ") + password = stdiomask.getpass(prompt="Amazon Password: ") + return { + "username": username, + "password": password, + } + + +def get_credentials(credentials_file, encrypted_pass=None): + if os.path.exists(credentials_file): + credential = load_encrypted_config(credentials_file, encrypted_pass) + return credential["username"], credential["password"] + else: + log.info("No credential file found, let's make one") + log.info("NOTE: DO NOT SAVE YOUR CREDENTIALS IN CHROME, CLICK NEVER!") + credential = await_credential_input() + create_encrypted_config(credential, credentials_file) + return credential["username"], credential["password"] + + +class Config: + def __init__(self) -> None: + super().__init__() + log.info("Initializing Global configuration...") + # Load up the global configuration + # See http://docs.red-dove.com/cfg/python.html#getting-started-with-cfg-in-python for how to use Config + self.global_config = config.Config(GLOBAL_CONFIG_FILE) + + def get_amazon_config(self, encryption_pass=None): + log.info("Initializing Amazon configuration...") + # Load up all things Amazon + amazon_config = self.global_config["AMAZON"] + amazon_config["username"], amazon_config["password"] = get_credentials( + AMAZON_CREDENTIAL_FILE, encryption_pass + ) + return amazon_config diff --git a/config/fairgame.conf b/config/fairgame.conf new file mode 100644 index 00000000..ad27ae1b --- /dev/null +++ b/config/fairgame.conf @@ -0,0 +1,110 @@ +{ + "AMAZON": { + "SIGN_IN_TEXT": [ + "Hello, Sign in", + "Sign in", + "Hola, Identifícate", + "Bonjour, Identifiez-vous", + "Ciao, Accedi", + "Hallo, Anmelden", + "Hallo, Inloggen" + ], + "SIGN_IN_TITLES": [ + "Amazon Sign In", + "Amazon Sign-In", + "Amazon Anmelden", + "Iniciar sesión en Amazon", + "Connexion Amazon", + "Amazon Accedi", + "Inloggen bij Amazon" + ], + "CAPTCHA_PAGE_TITLES": [ + "Robot Check" + ], + "HOME_PAGE_TITLES": [ + "Amazon.com: Online Shopping for Electronics, Apparel, Computers, Books, DVDs & more", + "AmazonSmile: You shop. Amazon gives.", + "Amazon.ca: Low Prices – Fast Shipping – Millions of Items", + "Amazon.co.uk: Low Prices in Electronics, Books, Sports Equipment & more", + "Amazon.de: Low Prices in Electronics, Books, Sports Equipment & more", + "Amazon.de: Günstige Preise für Elektronik & Foto, Filme, Musik, Bücher, Games, Spielzeug & mehr", + "Amazon.es: compra online de electrónica, libros, deporte, hogar, moda y mucho más.", + "Amazon.de: Günstige Preise für Elektronik & Foto, Filme, Musik, Bücher, Games, Spielzeug & mehr", + "Amazon.fr : livres, DVD, jeux vidéo, musique, high-tech, informatique, jouets, vêtements, chaussures, sport, bricolage, maison, beauté, puériculture, épicerie et plus encore !", + "Amazon.it: elettronica, libri, musica, fashion, videogiochi, DVD e tanto altro", + "Amazon.nl: Groot aanbod, kleine prijzen in o.a. Elektronica, boeken, sport en meer" + ], + "SHOPPING_CART_TITLES": [ + "Amazon.com Shopping Cart", + "Amazon.ca Shopping Cart", + "Amazon.co.uk Shopping Basket", + "Amazon.de Basket", + "Amazon.de Einkaufswagen", + "AmazonSmile Einkaufswagen", + "Cesta de compra Amazon.es", + "Amazon.fr Panier", + "Carrello Amazon.it", + "AmazonSmile Shopping Cart", + "AmazonSmile Shopping Basket", + "Amazon.nl-winkelwagen" + ], + "CHECKOUT_TITLES": [ + "Amazon.com Checkout", + "Amazon.co.uk Checkout", + "Place Your Order - Amazon.ca Checkout", + "Place Your Order - Amazon.co.uk Checkout", + "Amazon.de Checkout", + "Place Your Order - Amazon.de Checkout", + "Amazon.de - Bezahlvorgang", + "Bestellung aufgeben - Amazon.de-Bezahlvorgang", + "Place Your Order - Amazon.com Checkout", + "Place Your Order - Amazon.com", + "Tramitar pedido en Amazon.es", + "Processus de paiement Amazon.com", + "Confirmar pedido - Compra Amazon.es", + "Passez votre commande - Processus de paiement Amazon.fr", + "Ordina - Cassa Amazon.it", + "AmazonSmile Checkout", + "Plaats je bestelling - Amazon.nl-kassa", + "Place Your Order - AmazonSmile Checkout", + "Preparing your order", + "Ihre Bestellung wird vorbereitet" + ], + "ORDER_COMPLETE_TITLES": [ + "Amazon.com Thanks You", + "Amazon.ca Thanks You", + "AmazonSmile Thanks You", + "Thank you", + "Amazon.fr Merci", + "Merci", + "Amazon.es te da las gracias", + "Amazon.fr vous remercie.", + "Grazie da Amazon.it", + "Hartelijk dank", + "Thank You", + "Amazon.de Vielen Dank" + ], + "BUSINESS_PO_TITLES": [ + "Business order information" + ], + "DOGGO_TITLES": [ + "Sorry! Something went wrong!" + ], + "SHIPPING_ONLY_IF": "FREE Shipping on orders over", + "TWOFA_TITLES": [ + "Two-Step Verification" + ], + "PRIME_TITLES": [ + "Complete your Amazon Prime sign up" + ], + "OUT_OF_STOCK": [ + "Out of Stock - AmazonSmile Checkout" + ], + "NO_SELLERS": [ + "Currently, there are no sellers that can deliver this item to your location.", + "There are currently no listings for this search. Try a different refinement.", + "There are currently no listings for this search. Try a different refinement.", + "There are currently no listings for this product in . Try changing the condition type." + ] + } +} \ No newline at end of file diff --git a/stores/amazon.py b/stores/amazon.py index 4cc327a9..03bdc304 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1,18 +1,17 @@ +import fileinput import json import math import os import platform -import random import time from datetime import datetime -import fileinput import psutil -import stdiomask from amazoncaptcha import AmazonCaptcha from chromedriver_py import binary_path # this will get you the path variable from furl import furl from price_parser import parse_price +from pypresence import exceptions as pyexceptions from selenium import webdriver from selenium.common import exceptions from selenium.webdriver.common.keys import Keys @@ -20,12 +19,9 @@ from utils import discord_presence as presence from utils.debugger import debug -from utils.encryption import create_encrypted_config, load_encrypted_config from utils.logger import log from utils.selenium_utils import options, enable_headless -from pypresence import exceptions as pyexceptions - AMAZON_URLS = { "BASE_URL": "https://{domain}/", "OFFER_URL": "https://{domain}/gp/offer-listing/", @@ -34,123 +30,6 @@ CHECKOUT_URL = "https://{domain}/gp/cart/desktop/go-to-checkout.html/ref=ox_sc_proceed?partialCheckoutCart=1&isToBeGiftWrappedBefore=0&proceedToRetailCheckout=Proceed+to+checkout&proceedToCheckout=1&cartInitiateId={cart_id}" AUTOBUY_CONFIG_PATH = "config/amazon_config.json" -CREDENTIAL_FILE = "config/amazon_credentials.json" - -SIGN_IN_TEXT = [ - "Hello, Sign in", - "Sign in", - "Hola, Identifícate", - "Bonjour, Identifiez-vous", - "Ciao, Accedi", - "Hallo, Anmelden", - "Hallo, Inloggen", -] -SIGN_IN_TITLES = [ - "Amazon Sign In", - "Amazon Sign-In", - "Amazon Anmelden", - "Iniciar sesión en Amazon", - "Connexion Amazon", - "Amazon Accedi", - "Inloggen bij Amazon", -] -CAPTCHA_PAGE_TITLES = ["Robot Check"] -HOME_PAGE_TITLES = [ - "Amazon.com: Online Shopping for Electronics, Apparel, Computers, Books, DVDs & more", - "AmazonSmile: You shop. Amazon gives.", - "Amazon.ca: Low Prices – Fast Shipping – Millions of Items", - "Amazon.co.uk: Low Prices in Electronics, Books, Sports Equipment & more", - "Amazon.de: Low Prices in Electronics, Books, Sports Equipment & more", - "Amazon.de: Günstige Preise für Elektronik & Foto, Filme, Musik, Bücher, Games, Spielzeug & mehr", - "Amazon.es: compra online de electrónica, libros, deporte, hogar, moda y mucho más.", - "Amazon.de: Günstige Preise für Elektronik & Foto, Filme, Musik, Bücher, Games, Spielzeug & mehr", - "Amazon.fr : livres, DVD, jeux vidéo, musique, high-tech, informatique, jouets, vêtements, chaussures, sport, bricolage, maison, beauté, puériculture, épicerie et plus encore !", - "Amazon.it: elettronica, libri, musica, fashion, videogiochi, DVD e tanto altro", - "Amazon.nl: Groot aanbod, kleine prijzen in o.a. Elektronica, boeken, sport en meer", -] -SHOPING_CART_TITLES = [ - "Amazon.com Shopping Cart", - "Amazon.ca Shopping Cart", - "Amazon.co.uk Shopping Basket", - "Amazon.de Basket", - "Amazon.de Einkaufswagen", - "AmazonSmile Einkaufswagen", - "Cesta de compra Amazon.es", - "Amazon.fr Panier", - "Carrello Amazon.it", - "AmazonSmile Shopping Cart", - "AmazonSmile Shopping Basket", - "Amazon.nl-winkelwagen", -] -CHECKOUT_TITLES = [ - "Amazon.com Checkout", - "Amazon.co.uk Checkout", - "Place Your Order - Amazon.ca Checkout", - "Place Your Order - Amazon.co.uk Checkout", - "Amazon.de Checkout", - "Place Your Order - Amazon.de Checkout", - "Amazon.de - Bezahlvorgang", - "Bestellung aufgeben - Amazon.de-Bezahlvorgang", - "Place Your Order - Amazon.com Checkout", - "Place Your Order - Amazon.com", - "Tramitar pedido en Amazon.es", - "Processus de paiement Amazon.com", - "Confirmar pedido - Compra Amazon.es", - "Passez votre commande - Processus de paiement Amazon.fr", - "Ordina - Cassa Amazon.it", - "AmazonSmile Checkout", - "Plaats je bestelling - Amazon.nl-kassa", - "Place Your Order - AmazonSmile Checkout", - "Preparing your order", - "Ihre Bestellung wird vorbereitet", -] -ORDER_COMPLETE_TITLES = [ - "Amazon.com Thanks You", - "Amazon.ca Thanks You", - "AmazonSmile Thanks You", - "Thank you", - "Amazon.fr Merci", - "Merci", - "Amazon.es te da las gracias", - "Amazon.fr vous remercie.", - "Grazie da Amazon.it", - "Hartelijk dank", - "Thank You", - "Amazon.de Vielen Dank", -] -ADD_TO_CART_TITLES = [ - "Amazon.com: Please Confirm Your Action", - "Amazon.de: Bitte bestätigen Sie Ihre Aktion", - "Amazon.de: Please Confirm Your Action", - "Amazon.es: confirma tu acción", - "Amazon.com : Veuillez confirmer votre action", # Careful, required non-breaking space after .com ( ) - "Amazon.it: confermare l'operazione", - "AmazonSmile: Please Confirm Your Action", - "", # Amazon.nl has en empty title, sigh. -] -BUSINESS_PO_TITLES = [ - "Business order information", -] - -DOGGO_TITLES = ["Sorry! Something went wrong!"] - -# this is not non-US friendly -SHIPPING_ONLY_IF = "FREE Shipping on orders over" - -TWOFA_TITLES = ["Two-Step Verification"] - -PRIME_TITLES = ["Complete your Amazon Prime sign up"] - -OUT_OF_STOCK = ["Out of Stock - AmazonSmile Checkout"] - -NO_SELLERS = [ - "Currently, there are no sellers that can deliver this item to your location.", - "There are currently no listings for this search. Try a different refinement.", - "There are currently no listings for this search. Try a different refinement.", - "There are currently no listings for this product in . Try changing the condition type.", -] - -# OFFER_PAGE_TITLES = ["Amazon.com: Buying Choices:"] BUTTON_XPATHS = [ '//*[@id="submitOrderButtonId"]/span/input', @@ -180,6 +59,8 @@ DEFAULT_MAX_TIMEOUT = 10 DEFAULT_MAX_URL_FAIL = 5 +amazon_config = None + class Amazon: def __init__( @@ -193,8 +74,8 @@ def __init__( no_screenshots=False, disable_presence=False, slow_mode=False, - encryption_pass=None, no_image=False, + encryption_pass=None, ): self.notification_handler = notification_handler self.asin_list = [] @@ -216,8 +97,13 @@ def __init__( self.setup_driver = True self.headless = headless self.no_image = no_image - presence.enabled = not disable_presence + + global amazon_config + from cli.cli import global_config + + amazon_config = global_config.get_amazon_config(encryption_pass) + try: presence.start_presence() except Exception in pyexceptions: @@ -237,18 +123,6 @@ def __init__( except: raise - if os.path.exists(CREDENTIAL_FILE): - credential = load_encrypted_config(CREDENTIAL_FILE, encryption_pass) - self.username = credential["username"] - self.password = credential["password"] - else: - log.info("No credential file found, let's make one") - log.info("NOTE: DO NOT SAVE YOUR CREDENTIALS IN CHROME, CLICK NEVER!") - credential = self.await_credential_input() - create_encrypted_config(credential, CREDENTIAL_FILE) - self.username = credential["username"] - self.password = credential["password"] - if os.path.exists(AUTOBUY_CONFIG_PATH): with open(AUTOBUY_CONFIG_PATH) as json_file: try: @@ -280,15 +154,6 @@ def __init__( for key in AMAZON_URLS.keys(): AMAZON_URLS[key] = AMAZON_URLS[key].format(domain=self.amazon_website) - @staticmethod - def await_credential_input(): - username = input("Amazon login ID: ") - password = stdiomask.getpass(prompt="Amazon Password: ") - return { - "username": username, - "password": password, - } - def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): self.testing = test self.refresh_delay = delay @@ -396,7 +261,7 @@ def handle_startup(self): def is_logged_in(self): try: text = self.driver.find_element_by_id("nav-link-accountList").text - return not any(sign_in in text for sign_in in SIGN_IN_TEXT) + return not any(sign_in in text for sign_in in amazon_config["SIGN_IN_TEXT"]) except exceptions.NoSuchElementException: return False @@ -423,7 +288,7 @@ def login(self): if email_field: try: - email_field.send_keys(self.username + Keys.RETURN) + email_field.send_keys(amazon_config["username"] + Keys.RETURN) except exceptions.ElementNotInteractableException: log.info("Email not needed.") else: @@ -457,7 +322,7 @@ def login(self): if time.time() > timeout: break if password_field: - password_field.send_keys(self.password + Keys.RETURN) + password_field.send_keys(amazon_config["password"] + Keys.RETURN) self.wait_for_page_change(current_page) else: log.error("Password entry box did not exist") @@ -492,11 +357,11 @@ def login(self): log.debug("login page did not have captcha element") # time.sleep(self.page_wait_delay()) - if self.driver.title in TWOFA_TITLES: + if self.driver.title in amazon_config["TWOFA_TITLES"]: log.info("enter in your two-step verification code in browser") - while self.driver.title in TWOFA_TITLES: + while self.driver.title in amazon_config["WOFA_TITLES"]: time.sleep(0.2) - log.info(f"Logged in as {self.username}") + log.info(f'Logged in as {amazon_config["username"]}') @debug def run_asins(self, delay): @@ -589,7 +454,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): except exceptions.NoSuchElementException: pass - if test and (test.text in NO_SELLERS): + if test and (test.text in amazon_config["NO_SELLERS"]): return False if time.time() > timeout: log.info(f"failed to load page for {asin}, going to next ASIN") @@ -628,7 +493,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False try: if self.checkshipping: - if SHIPPING_ONLY_IF in shipping[idx].text: + if amazon_config["SHIPPING_ONLY_IF"] in shipping[idx].text: ship_price = parse_price("0") else: ship_price = parse_price(shipping[idx].text) @@ -671,7 +536,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False self.wait_for_page_change(current_title) # log.info(f"page title is {self.driver.title}") - if self.driver.title in SHOPING_CART_TITLES: + if self.driver.title in amazon_config["SHOPPING_CART_TITLES"]: return True else: log.info("did not add to cart, trying again") @@ -705,26 +570,26 @@ def navigate_pages(self, test): # time.sleep(self.page_wait_delay()) title = self.driver.title - if title in SIGN_IN_TITLES: + if title in amazon_config["SIGN_IN_TITLES"]: self.login() - elif title in CAPTCHA_PAGE_TITLES: + elif title in amazon_config["CAPTCHA_PAGE_TITLES"]: self.handle_captcha() - elif title in SHOPING_CART_TITLES: + elif title in amazon_config["SHOPPING_CART_TITLES"]: self.handle_cart() - elif title in CHECKOUT_TITLES: + elif title in amazon_config["CHECKOUT_TITLES"]: self.handle_checkout(test) - elif title in ORDER_COMPLETE_TITLES: + elif title in amazon_config["ORDER_COMPLETE_TITLES"]: self.handle_order_complete() - elif title in PRIME_TITLES: + elif title in amazon_config["PRIME_TITLES"]: self.handle_prime_signup() - elif title in HOME_PAGE_TITLES: + elif title in amazon_config["HOME_PAGE_TITLES"]: # if home page, something went wrong self.handle_home_page() - elif title in DOGGO_TITLES: + elif title in amazon_config["DOGGO_TITLES"]: self.handle_doggos() - elif title in OUT_OF_STOCK: + elif title in amazon_config["OUT_OF_STOCK"]: self.handle_out_of_stock() - elif title in BUSINESS_PO_TITLES: + elif title in amazon_config["BUSINESS_PO_TITLES"]: self.handle_business_po() else: log.error( @@ -796,7 +661,7 @@ def handle_prime_signup(self): "Prime offer page popped up, user intervention required" ) timeout = self.get_timeout(timeout=60) - while self.driver.title in PRIME_TITLES: + while self.driver.title in amazon_config["PRIME_TITLES"]: if time.time() > timeout: log.info( "user did not intervene in time, will try and refresh page" @@ -1054,7 +919,7 @@ def get_webdriver_pids(self): self.webdriver_child_pids.append(child.pid) def get_page(self, url): - check_cart_element = [] + check_cart_element = None current_page = [] try: check_cart_element = self.driver.find_element_by_xpath( @@ -1088,7 +953,9 @@ def __del__(self): def show_config(self): log.info(f"{'=' * 50}") - log.info(f"Starting Amazon ASIN Hunt for {len(self.asin_list)} Products with:") + log.info( + f"Starting Amazon ASIN Hunt on {AMAZON_URLS['BASE_URL']} for {len(self.asin_list)} Products with:" + ) log.info(f"--Delay of {self.refresh_delay} seconds") if self.used: log.info(f"--Used items are considered for purchase") diff --git a/utils/logger.py b/utils/logger.py index 8fd7bd6f..c949c9d0 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -46,4 +46,4 @@ log.addHandler(stream_handler) -coloredlogs.install(LOGLEVEL, logger=log) +coloredlogs.install(LOGLEVEL, logger=log, fmt="%(asctime)s %(levelname)s - %(message)s") diff --git a/utils/version.py b/utils/version.py index 1dbb6f3c..5f6bf58e 100644 --- a/utils/version.py +++ b/utils/version.py @@ -1,24 +1,42 @@ import requests +from packaging.version import Version, parse, InvalidVersion from utils.logger import log -LATEST_URL = "https://api.github.com/repos/Hari-Nagarajan/fairgame/releases/latest" +_LATEST_URL = "https://api.github.com/repos/Hari-Nagarajan/fairgame/releases/latest" -version = "0.5.3" +# Use a Version object to gain additional version identification capabilities +# See https://github.com/pypa/packaging for details +# See https://www.python.org/dev/peps/pep-0440/ for specification +# See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples + +__VERSION = "0.6.0.dev1" +version = Version(__VERSION) def check_version(): + remote_version = get_latest_version() + + if version < remote_version: + log.warning( + f"You are running FairGame v{version.release}, but the most recent version is v{remote_version.release}. " + f"Consider upgrading " + ) + elif version.is_prerelease: + log.warning(f"FairGame PRE-RELEASE v{version}") + else: + log.info(f"FairGame v{version}") + + +def get_latest_version(): try: - r = requests.get(LATEST_URL) + r = requests.get(_LATEST_URL) data = r.json() - remote_version = str(data["tag_name"]) - - if version < remote_version: - log.warning( - f"You are running FairGame v{version}, but the most recent version is v{remote_version}... Consider upgrading" - ) - else: - log.info(f"FairGame v{version}") - except: - log.error("Failed version check. Continuing execution with mystery code.") - pass + latest_version = parse(str(data["tag_name"])) + except InvalidVersion: + # Return a safe, but wrong version1 + latest_version = parse("0.0") + log.error( + f"Failed complete check for latest version. Assuming v{latest_version}" + ) + return latest_version From 5296fc7aee5a0f2c59c61209e7e58a5c2ec2d167 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sun, 27 Dec 2020 15:02:43 -0500 Subject: [PATCH 003/150] Updates amazon configuration console output. Removed dash after log level in console logger --- stores/amazon.py | 12 +++++++++--- utils/logger.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 03bdc304..4778ae0f 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -964,15 +964,13 @@ def show_config(self): else: log.info(f"--Free Shipping items only") if self.single_shot: - log.info("\tSingle Shot purchase enabled") + log.info("--Single Shot purchase enabled") if not self.take_screenshots: log.info( f"--Screenshotting is Disabled, DO NOT ASK FOR HELP IN TECH SUPPORT IF YOU HAVE NO SCREENSHOTS!" ) if self.detailed: log.info(f"--Detailed screenshots/notifications is enabled") - if self.testing: - log.warning(f"--Testing Mode. NO Purchases will be made.") if self.slow_mode: log.warning(f"--Slow-mode enabled. Pages will fully load before execution") @@ -980,6 +978,14 @@ def show_config(self): log.info( f"--Looking for {len(asins)} ASINs between {self.reserve_min[idx]:.2f} and {self.reserve_max[idx]:.2f}" ) + if self.no_image: + log.info(f"--No images will be requested") + if not self.notification_handler.sound_enabled: + log.info(f"--Notification sounds are disabled.") + if self.headless: + log.warning(f"--Running headless is unsupported. If you get it to work, please let us know on Discord.") + if self.testing: + log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") def create_driver(self): diff --git a/utils/logger.py b/utils/logger.py index c949c9d0..c165abdd 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -46,4 +46,4 @@ log.addHandler(stream_handler) -coloredlogs.install(LOGLEVEL, logger=log, fmt="%(asctime)s %(levelname)s - %(message)s") +coloredlogs.install(LOGLEVEL, logger=log, fmt="%(asctime)s %(levelname)s %(message)s") From 1f0d77ba4730fb30215f0d438f8385875ec0f0b3 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sun, 27 Dec 2020 15:03:07 -0500 Subject: [PATCH 004/150] Blackd --- stores/amazon.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index 4778ae0f..188e2591 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -983,7 +983,9 @@ def show_config(self): if not self.notification_handler.sound_enabled: log.info(f"--Notification sounds are disabled.") if self.headless: - log.warning(f"--Running headless is unsupported. If you get it to work, please let us know on Discord.") + log.warning( + f"--Running headless is unsupported. If you get it to work, please let us know on Discord." + ) if self.testing: log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") From 7dd2e80e3b1b935e94575f07beb7c8a34a6f4cf9 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 28 Dec 2020 17:16:14 -0500 Subject: [PATCH 005/150] --Updated logging format to include version. --Refactored Version to remove dependency on logger.py --- cli/cli.py | 13 +++++++++++-- utils/logger.py | 16 +++++++++------- utils/version.py | 18 +++++------------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/cli/cli.py b/cli/cli.py index 3d7bc212..1d04c41b 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -6,7 +6,7 @@ from notifications.notifications import NotificationHandler, TIME_FORMAT from utils.logger import log from common.config import Config -from utils.version import check_version +from utils.version import is_latest, version from stores.amazon import Amazon from stores.bestbuy import BestBuyHandler @@ -38,7 +38,16 @@ def main(): global global_config global notification_handler # Global scope stuff here - check_version() + if is_latest(): + log.info(f"FairGame v{version}") + elif version.is_prerelease: + log.warning(f"FairGame PRE-RELEASE v{version}") + else: + log.warning( + f"You are running FairGame v{version.release}, but the most recent version is v{remote_version.release}. " + f"Consider upgrading " + ) + global_config = Config() notification_handler = NotificationHandler() pass diff --git a/utils/logger.py b/utils/logger.py index c165abdd..0206b6ee 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -1,9 +1,11 @@ import coloredlogs import logging import os - +from utils.version import version from logging import handlers +FORMAT = "%(asctime)s|{}|%(levelname)s|%(message)s".format(version) + LOG_DIR = "logs" LOG_FILE_NAME = "fairgame.log" if not os.path.exists(LOG_DIR): @@ -20,7 +22,9 @@ if os.path.isfile(LOG_FILE_PATH): # Create a transient handler to do the rollover for us on startup. This won't # be added to the logger as a handler... just used to roll the log on startup. - rollover_handler = handlers.RotatingFileHandler(LOG_FILE_PATH, backupCount=10) + rollover_handler = handlers.RotatingFileHandler( + LOG_FILE_PATH, backupCount=10, maxBytes=100 * 1024 * 1024 + ) # Prior log file exists, so roll it to get a clean log for this run try: rollover_handler.doRollover() @@ -32,7 +36,7 @@ logging.basicConfig( filename=LOG_FILE_PATH, level=logging.DEBUG, - format='%(levelname)s: "%(asctime)s - %(message)s', + format=FORMAT, ) log = logging.getLogger("fairgame") @@ -40,10 +44,8 @@ LOGLEVEL = os.environ.get("LOGLEVEL", "INFO").upper() stream_handler = logging.StreamHandler() -stream_handler.setFormatter( - logging.Formatter('%(levelname)s: "%(asctime)s - %(message)s') -) +stream_handler.setFormatter(logging.Formatter(FORMAT)) log.addHandler(stream_handler) -coloredlogs.install(LOGLEVEL, logger=log, fmt="%(asctime)s %(levelname)s %(message)s") +coloredlogs.install(LOGLEVEL, logger=log, fmt=FORMAT) diff --git a/utils/version.py b/utils/version.py index 5f6bf58e..8fc65b24 100644 --- a/utils/version.py +++ b/utils/version.py @@ -1,8 +1,6 @@ import requests from packaging.version import Version, parse, InvalidVersion -from utils.logger import log - _LATEST_URL = "https://api.github.com/repos/Hari-Nagarajan/fairgame/releases/latest" # Use a Version object to gain additional version identification capabilities @@ -14,18 +12,15 @@ version = Version(__VERSION) -def check_version(): +def is_latest(): remote_version = get_latest_version() if version < remote_version: - log.warning( - f"You are running FairGame v{version.release}, but the most recent version is v{remote_version.release}. " - f"Consider upgrading " - ) + return False elif version.is_prerelease: - log.warning(f"FairGame PRE-RELEASE v{version}") + return False else: - log.info(f"FairGame v{version}") + return True def get_latest_version(): @@ -34,9 +29,6 @@ def get_latest_version(): data = r.json() latest_version = parse(str(data["tag_name"])) except InvalidVersion: - # Return a safe, but wrong version1 + # Return a safe, but wrong version latest_version = parse("0.0") - log.error( - f"Failed complete check for latest version. Assuming v{latest_version}" - ) return latest_version From 3263db2725193e51123fd39aa5470ed8d301a1a2 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 28 Dec 2020 17:29:51 -0500 Subject: [PATCH 006/150] --Added link to explain Amazon Smile --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9553d08..708d1ade 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ are interested in purchasing. [*What's an ASIN?*](https://www.datafeedwatch.com * Use sequential numbers for x, starting from 1. x can be any integer from 1 to 18,446,744,073,709,551,616 * `reserve_min_x` set a minimum limit to consider for purchasing an item. If a seller has a listing for a 700 dollar item a 1 dollar, it's likely fake. * `reserve_max_x` is the most amount you want to spend for a single item (i.e., ASIN) in `asin_list_x`. Does not include tax. If --checkshipping flag is active, this includes shipping listed on offer page. -* `amazon_website` amazon domain you want to use. smile subdomain appears to work better, if available in your country. +* `amazon_website` amazon domain you want to use. smile subdomain appears to work better, if available in your country. [*What is Smile?*](https://org.amazon.com/) **Examples** From 4a4477fda9ec1f67f60d02b51790cadbd8c01eb4 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 29 Dec 2020 11:08:10 -0500 Subject: [PATCH 007/150] --Refactored Config class to GlobalConfig for namespacing --- cli/cli.py | 4 ++-- common/{config.py => globalconfig.py} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename common/{config.py => globalconfig.py} (93%) diff --git a/cli/cli.py b/cli/cli.py index 1d04c41b..282215ae 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -5,7 +5,7 @@ from signal import signal, SIGINT from notifications.notifications import NotificationHandler, TIME_FORMAT from utils.logger import log -from common.config import Config +from common.globalconfig import GlobalConfig from utils.version import is_latest, version from stores.amazon import Amazon from stores.bestbuy import BestBuyHandler @@ -48,7 +48,7 @@ def main(): f"Consider upgrading " ) - global_config = Config() + global_config = GlobalConfig() notification_handler = NotificationHandler() pass diff --git a/common/config.py b/common/globalconfig.py similarity index 93% rename from common/config.py rename to common/globalconfig.py index ff497549..bca77217 100644 --- a/common/config.py +++ b/common/globalconfig.py @@ -1,5 +1,5 @@ import os -import config +from config import Config as Cfg import stdiomask from utils.encryption import load_encrypted_config, create_encrypted_config @@ -30,13 +30,13 @@ def get_credentials(credentials_file, encrypted_pass=None): return credential["username"], credential["password"] -class Config: +class GlobalConfig: def __init__(self) -> None: super().__init__() log.info("Initializing Global configuration...") # Load up the global configuration # See http://docs.red-dove.com/cfg/python.html#getting-started-with-cfg-in-python for how to use Config - self.global_config = config.Config(GLOBAL_CONFIG_FILE) + self.global_config = Cfg(GLOBAL_CONFIG_FILE) def get_amazon_config(self, encryption_pass=None): log.info("Initializing Amazon configuration...") From 77e30e3de7707febacb8f2ec9f85da15602d02d1 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 29 Dec 2020 11:16:37 -0500 Subject: [PATCH 008/150] Reworked startup code to expose global variables after intialization. --- cli/cli.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/cli/cli.py b/cli/cli.py index 282215ae..473b7af9 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -10,9 +10,6 @@ from stores.amazon import Amazon from stores.bestbuy import BestBuyHandler -global_config = None -notification_handler = None - def handler(signal, frame): log.info("Caught the stop, exiting.") @@ -35,21 +32,6 @@ def decorator(*args, **kwargs): @click.group() def main(): - global global_config - global notification_handler - # Global scope stuff here - if is_latest(): - log.info(f"FairGame v{version}") - elif version.is_prerelease: - log.warning(f"FairGame PRE-RELEASE v{version}") - else: - log.warning( - f"You are running FairGame v{version.release}, but the most recent version is v{remote_version.release}. " - f"Consider upgrading " - ) - - global_config = GlobalConfig() - notification_handler = NotificationHandler() pass @@ -222,3 +204,17 @@ def test_notifications(disable_sound): main.add_command(amazon) main.add_command(bestbuy) main.add_command(test_notifications) + +# Global scope stuff here +if is_latest(): + log.info(f"FairGame v{version}") +elif version.is_prerelease: + log.warning(f"FairGame PRE-RELEASE v{version}") +else: + log.warning( + f"You are running FairGame v{version.release}, but the most recent version is v{remote_version.release}. " + f"Consider upgrading " + ) + +global_config = GlobalConfig() +notification_handler = NotificationHandler() From f510796362135f36fc500b2041e5eefe9b937202 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+dakkjaniels@users.noreply.github.com> Date: Mon, 4 Jan 2021 17:40:35 -0500 Subject: [PATCH 009/150] Surrogate commit for DakkJaniels implementation of pathing for Chrome profile using running app's absolute file path. Solves --headless on at least Linux. --- stores/amazon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index 4b7c9f7e..36c59761 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1229,7 +1229,10 @@ def create_driver(self): else: prefs["profile.managed_default_content_settings.images"] = 0 options.add_experimental_option("prefs", prefs) - options.add_argument(f"user-data-dir=.profile-amz") + path_to_profile = os.path.join( + os.path.dirname(os.path.abspath("__file__")), ".profile-amz" + ) + options.add_argument(f"user-data-dir={path_to_profile}") if not self.slow_mode: options.set_capability("pageLoadStrategy", "none") From 9065b462ef298777d69d9e079d5336dd22896ded Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 4 Jan 2021 18:40:37 -0500 Subject: [PATCH 010/150] Correctly disabled sounds only instead of notifications altogether when a sound device isn't present. --- notifications/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notifications/notifications.py b/notifications/notifications.py index cec7245a..86f4be2f 100644 --- a/notifications/notifications.py +++ b/notifications/notifications.py @@ -73,4 +73,4 @@ def play(self, audio_file=None, **kwargs): log.warn( "Error playing notification sound. Disabling local audio notifications." ) - self.enabled = False + self.sound_enabled = False From b57e1217e45e6704f209719998b97158576ce6ce Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 10:11:11 -0500 Subject: [PATCH 011/150] Interim check-in of updates to platforms. Additional headings tagged and content reorganized a bit. --- README.md | 392 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 285 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 16753bac..39c8c12a 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,71 @@ # Fairgame -[Installation](#Installation) | [Usage](#Usage) | [Discord](https://discord.gg/qDY2QBtAW6) | [Troubleshooting](#Troubleshooting) +[Installation](#Installation) | [Usage](#Usage) | [Discord](https://discord.gg/4rfbNKrmnC) +| [Troubleshooting](#Troubleshooting) ## Why??? -We built this in response to the severe tech scalping situation that's happening right now. Almost every tech product that's coming -out right now is being instantly brought out by scalping groups and then resold at at insane prices. $699 GPUs are being listed -for $1700 on eBay, and these scalpers are buying 40 cards while normal consumers can't get a single one. Preorders for the PS5 are -being resold for nearly $1000. Our take on this is that if we release a bot that anyone can use, for free, then the number of items -that scalpers can buy goes down and normal consumers can buy items for MSRP. +We built this in response to the severe tech scalping situation that's happening right now. Almost every tech product +that's coming out right now is being instantly brought out by scalping groups and then resold at at insane prices. $699 +GPUs are being listed for $1700 on eBay, and these scalpers are buying 40 cards while normal consumers can't get a +single one. Preorders for the PS5 are being resold for nearly $1000. Our take on this is that if we release a bot that +anyone can use, for free, then the number of items that scalpers can buy goes down and normal consumers can buy items +for MSRP. **If everyone is botting, then no one is botting.** +## Current Functionality + +| **Website** | **Auto Checkout** | **Open Cart Link** | **Test flag** | +|:---:|:---:|:---:|:---:| +| amazon.com |`✔`| | | +| ~~bestbuy.com~~ | |`✔`| | + +Best Buy has been deprecated, see [details](#best-buy) below. + ## Got a question? -Read through this document and the cheat sheet linked in the next sections. See the [FAQs](#frequently-asked-questions) if that does not answer your questions. +Read through this document and the cheat sheet linked in the next sections. See the [FAQs](#frequently-asked-questions) +if that does not answer your questions. ## Installation -Easy_XII has created a great cheat sheet for getting started, [please follow this guide](https://docs.google.com/document/d/1grN282tPodM9N57bPq4bbNyKZC01t_4A-sLpzzu_7lM/). -**Note:** that we do not control the contents of this document, so use some common sense when configuring the bot. Do not ask us -why the bot does not purchase an $8.49 item when the minimum purchase price is set to $10 in the configuration file that YOU are supposed to update +Community user Easy_XII has created a great cheat sheet for getting started. It includes specific and additional steps +for Windows users as well as useful product and configuration information. Please start +with [this guide](https://docs.google.com/document/d/1grN282tPodM9N57bPq4bbNyKZC01t_4A-sLpzzu_7lM/) to get you started +and to answer any initial questions you may have about setup. + +**Note:** The above document is community maintained and managed. The authors of Fairgame do not control the contents, +so use some common sense when configuring the bot as both the bot and the sites we interact with change over time. For +example, do not ask us why the bot does not purchase an item whose price has changed to $8.49 when the _minimum_ +purchase price is set to $10 in the configuration file that YOU are supposed to update + +### General + +This project uses [Pipenv](https://pypi.org/project/pipenv/) to manage dependencies. Hop in +my [Discord](https://discord.gg/4rfbNKrmnC) if you have ideas, need help or just want to tell us about how you got your +new toys. + +To get started, there are two options: + +#### Releases -This project uses [Pipenv](https://pypi.org/project/pipenv/) to manage dependencies. Hop in my [Discord](https://discord.gg/qDY2QBtAW6) if you have ideas, need help or just want to tell us about how you got your new toys. +To get the latest release as a convenient package, download it directly from +the [Releases](https://github.com/Hari-Nagarajan/fairgame/releases) +page on GitHub. The "Source code" zip or tar file are what you'll want. This can be downloaded and extracted into a +directory of your choice (e.g. C:\fairgame). -To get started you'll first need to clone this repository. If you are unfamiliar with Git, follow the [guide on how to do that on our Wiki](https://github.com/Hari-Nagarajan/fairgame/wiki/How-to-use-GitHub-Desktop-App). You *can* use the "Download Zip" button on the GitHub repository's homepage but this makes receieving updates more difficult. If you can get setup with the GitHub Desktop app, updating to the latest version of the bot takes 1 click. +#### Git + +If you want to manage the code via Git, you'll first need to clone this repository. If you are unfamiliar with Git, +follow the [guide](https://github.com/Hari-Nagarajan/fairgame/wiki/How-to-use-GitHub-Desktop-App) on how to do that on +our Wiki . You *can* use the "Download Zip" button on the GitHub repository's homepage but this makes receiving updates +more difficult. If you can get setup with the GitHub Desktop app, updating to the latest version of the bot takes 1 +click. !!! YOU WILL NEED TO USE THE 3.8 BRANCH OF PYTHON, 3.9.0 BREAKS DEPENDENCIES !!! -``` + +```shell pip install pipenv pipenv shell pipenv install @@ -36,7 +74,8 @@ pipenv install NOTE: YOU SHOULD RUN `pipenv shell` and `pipenv install` ANY TIME YOU UPDATE, IN CASE THE DEPENDENCIES HAVE CHANGED! Run it -``` + +```shell python app.py Usage: app.py [OPTIONS] COMMAND [ARGS]... @@ -48,34 +87,126 @@ Commands: amazon ``` -## Current Functionality +### Platform Specific -| **Website** | **Auto Checkout** | **Open Cart Link** | **Test flag** | -|:---:|:---:|:---:|:---:| -| amazon.com |`✔`| | | -| ~~bestbuy.com~~ | |`✔`| | -Best Buy has been deprecated, see details below. +These instructions are supplied by community members and any adjustments, corrections, improvements or clarifications +are welcome. These are typically created during installation in a single environment, so there may be caveats or changes +necessary for your environment. This isn't intended to be a definitive guide, but a starting point as validation that a +platform can/does work. Please report back any suggestions to our [Discord](https://discord.gg/qDY2QBtAW6) feedback +channel. + +### Installation Ubuntu 20.10 (and probably other distros) + +Based off Ubuntu 20.10 with a fresh installation. + +Open terminal. Either right click desktop and go to Open In Terminal, or search for Terminal under Show Applications + +Install Google Chrome: +`wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && sudo dpkg -i google-chrome-stable_current_amd64.deb` + +Install Pip: +`sudo apt install python3-pip` + +Install pipenv: +`pip3 install pipenv` + +Add /home/$USER/.local/bin to PATH: +`export PATH="/home/$USER/.local/bin:$PATH"` + +Install git: +`sudo apt install git` + +Clone git repository: +`git clone https://github.com/Hari-Nagarajan/fairgame` + +Change into the fairgame folder: +`cd ./fairgame/` + +Prepare your config files within ./config/ + +```shell +cp ./config/amazon_config.template_json ./config/amazon_config.json +cp ./config/apprise.conf_template ./config/apprise.conf +``` + +Make a pipshell environment: +`pipenv shell` + +Install dependencies: +`pipenv install` + +Edit the newly created files with your settings based on your [configuration](#configuration) + +### Installation Raspberry Pi 4 (2 GB+) + +This is an abridged version of the community created document +found [here](https://docs.google.com/document/d/1VUxXhATZ8sZOJxdh3AIY6OGqwLRmrAcPikKZAwphIE8/edit). If the steps here +don't work on your Pi 4, look there for additional options. This hasn't been tested on a Pi 3, but given enough RAM to +run Chrome, it may very well work. Let us know. + +```shell +sudo apt update +sudo apt upgrade +sudo apt install chromium-chromedriver +git clone https://github.com/Hari-Nagarajan/fairgame +cd fairgame/ +pip3 install pipenv +export PATH=$PATH:/home/$USER/.local/bin +pipenv shell +pipenv install +``` + +Leave this Terminal window open. + +Open the following file in a text editor: + +`/home/$USER/.local/share/virtualenvs/fairgame-/lib/python3.7/site-packages/selenium/webdriver/common/service.py` + +Edit line 38 from + +`self.path = executable` + +to + +`self.path = "chromedriver"` + +Then save and close the file. + +Additional steps outside of the readme: + +uncomment requires 3.8 Piplock, as default pi python 2.7.16 works fine + +If you get a compiler error when doing pipenv install run: + +```shell +sudo python3 -m pip install --upgrade pip +sudo apt-get install libffi-dev +sudo apt-get install libzbar-dev +sudo apt-get install clang -y +``` ## Usage -### Amazon -The following flags are specific to the Amazon scripts. They the `[OPTIONS]` to be passed on the command-line to control -the behavior of Amazon scanning and purchasing. These can be added at the command line or added to a batch file/shell - script (see `_Amazon.bat` in the root folder of the project). **NOTE:** `--test` flag has been added to `_Amazon.bat` file by -default. This should be deleted after you've verified that the bot works correctly for you. If you don't want your `_Amazon.bat` +### Amazon + +The following flags are specific to the Amazon scripts. They the `[OPTIONS]` to be passed on the command-line to control +the behavior of Amazon scanning and purchasing. These can be added at the command line or added to a batch file/shell +script (see `_Amazon.bat` in the root folder of the project). **NOTE:** `--test` flag has been added to `_Amazon.bat` +file by default. This should be deleted after you've verified that the bot works correctly for you. If you don't want +your `_Amazon.bat` to be deleted when you update, you should rename it to something else. -**Amazon flags** +#### Amazon flags #### -``` +```shell python app.py amazon --help -Usage: app.py amazon [OPTIONS] +Usage: app.py amazon option Options: --no-image Do not load images --headless Unsupported headless mode. GLHF - --test Run the checkout flow, but do not actually purchase the + --test Run the checkout flow but do not actually purchase the item[s] --delay FLOAT Time to wait between checks for item[s] @@ -109,31 +240,38 @@ Options: --help Show this message and exit. ``` -**Configuration** +#### Configuration -Make a copy of `amazon_config.template_json` and rename to `amazon_config.json`. Edit it according to the ASINs you -are interested in purchasing. [*What's an ASIN?*](https://www.datafeedwatch.com/blog/amazon-asin-number-what-is-it-and-how-do-you-get-it#how-to-find-asin) +Make a copy of `amazon_config.template_json` and rename to `amazon_config.json`. Edit it according to the ASINs you are +interested in purchasing. [*What's an +ASIN?*](https://www.datafeedwatch.com/blog/amazon-asin-number-what-is-it-and-how-do-you-get-it#how-to-find-asin) * `asin_groups` indicates the number of ASIN groups you want to use. -* `asin_list_x` list of ASINs for products you want to purchase. You must locate these (see Discord or lookup the ASIN on product pages). - * The first time an item from list "x" is in stock and under its associated reserve, it will purchase it. +* `asin_list_x` list of ASINs for products you want to purchase. You must locate these (see Discord or lookup the ASIN + on product pages). + * The first time an item from list "x" is in stock and under its associated reserve, it will purchase it. * If the purchase is successful, the bot will not buy anything else from list "x". * Use sequential numbers for x, starting from 1. x can be any integer from 1 to 18,446,744,073,709,551,616 -* `reserve_min_x` set a minimum limit to consider for purchasing an item. If a seller has a listing for a 700 dollar item a 1 dollar, it's likely fake. -* `reserve_max_x` is the most amount you want to spend for a single item (i.e., ASIN) in `asin_list_x`. Does not include tax. If --checkshipping flag is active, this includes shipping listed on offer page. -* `amazon_website` amazon domain you want to use. smile subdomain appears to work better, if available in your country. [*What is Smile?*](https://org.amazon.com/) +* `reserve_min_x` set a minimum limit to consider for purchasing an item. If a seller has a listing for a 700 dollar + item a 1 dollar, it's likely fake. +* `reserve_max_x` is the most amount you want to spend for a single item (i.e., ASIN) in `asin_list_x`. Does not include + tax. If --checkshipping flag is active, this includes shipping listed on offer page. +* `amazon_website` amazon domain you want to use. smile subdomain appears to work better, if available in your + country. [*What is Smile?*](https://org.amazon.com/) -**Examples** +##### Examples One unique product with one ASIN (e.g., Segway Ninebot S and GoKart Drift Kit Bundle) : ```json { - "asin_groups": 1, - "asin_list_1": ["B07K7NLDGT"], - "reserve_min_1": 450, - "reserve_max_1": 500, - "amazon_website": "smile.amazon.com" + "asin_groups": 1, + "asin_list_1": [ + "B07K7NLDGT" + ], + "reserve_min_1": 450, + "reserve_max_1": 500, + "amazon_website": "smile.amazon.com" } ``` @@ -141,44 +279,57 @@ One general product with multiple ASINS (e.g 16 GB USB drive 2 pack) ```json { - "asin_groups": 1, - "asin_list_1": ["B07JH53M4T", "B085M1SQ9S", "B00E9W1ULS"], - "reserve_min_1": 15, - "reserve_max_1": 20, - "amazon_website": "smile.amazon.com" + "asin_groups": 1, + "asin_list_1": [ + "B07JH53M4T", + "B085M1SQ9S", + "B00E9W1ULS" + ], + "reserve_min_1": 15, + "reserve_max_1": 20, + "amazon_website": "smile.amazon.com" } ``` -Two general products with multiple ASINS and different price points (e.g. 16 GB USB drive 2 pack and a statue of The Thinker) +Two general products with multiple ASINS and different price points (e.g. 16 GB USB drive 2 pack and a statue of The +Thinker) ```json { - "asin_groups": 2, - "asin_list_1": ["B07JH53M4T", "B085M1SQ9S", "B00E9W1ULS"], - "reserve_min_1": 15, - "reserve_max_1": 20, - "asin_list_2": ["B006HPI2A2", "B00N54S1WW"], - "reserve_min_2": 50, - "reserve_max_2": 75, - "amazon_website": "smile.amazon.com" + "asin_groups": 2, + "asin_list_1": [ + "B07JH53M4T", + "B085M1SQ9S", + "B00E9W1ULS" + ], + "reserve_min_1": 15, + "reserve_max_1": 20, + "asin_list_2": [ + "B006HPI2A2", + "B00N54S1WW" + ], + "reserve_min_2": 50, + "reserve_max_2": 75, + "amazon_website": "smile.amazon.com" } ``` If you wanted to watch another product, you'd add a third list (e.g. `asin_list_3`) and associated min/max pricing and -increase the `asin_groups` to 3. Add as many lists as are needed, keeping in mind that the main distinction between lists -is the min/max price boundaries. Once any ASIN is purchased from an ASIN list, that list is remove from the hunt +increase the `asin_groups` to 3. Add as many lists as are needed, keeping in mind that the main distinction between +lists is the min/max price boundaries. Once any ASIN is purchased from an ASIN list, that list is remove from the hunt until FairGame is restarted. To verify that your JSON is well formatted, paste and validate it at https://jsonlint.com/ -**Start Up** +#### Start Up -Previously your username and password were entered into the config file, this is no longer the case. On first launch the bot will prompt -you for your credentials. You will then be asked for a password to encrypt them. Once done, your encrypted credentials will be stored in -`amazon_credentials.json`. If you ever forget your encryption password, just delete this file and the next launch of the bot will recreate -it. An example of this will look like the following: +Previously your username and password were entered into the config file, this is no longer the case. On first launch the +bot will prompt you for your credentials. You will then be asked for a password to encrypt them. Once done, your +encrypted credentials will be stored in +`amazon_credentials.json`. If you ever forget your encryption password, just delete this file and the next launch of the +bot will recreate it. An example of this will look like the following: -``` +```shell python app.py amazon INFO Initializing Apprise handler INFO Initializing other notification handlers @@ -194,7 +345,7 @@ INFO Credentials safely stored. Starting the bot when you have created an encrypted file: -``` +```shell python app.py amazon --test INFO Initializing Apprise handler INFO Initializing other notification handlers @@ -202,9 +353,10 @@ INFO Enabled Handlers: ['Audio'] Reading credentials from: amazon_credentials.json Credential file password: ``` + Example usage: -``` +```commandline python app.py amazon --test ... 2020-12-23 13:07:38 INFO Initializing Apprise handler using: config/apprise.conf @@ -237,47 +389,50 @@ python app.py amazon --test ``` - ## ~~Best Buy~~ -Best Buy is currently deprecated because we don't yet have an effective way to determine item availability -without scraping and processing the product pages individually. Future updates may see this functionality -return, but the current code isn't reliable for high demand items and checkout automation has become -increasingly hard due to anti-bot measures taken by Best Buy. +Best Buy is currently deprecated because we don't yet have an effective way to determine item availability without +scraping and processing the product pages individually. Future updates may see this functionality return, but the +current code isn't reliable for high demand items and checkout automation has become increasingly hard due to anti-bot +measures taken by Best Buy. -Original code still exists, but provides very little utility. A 3rd party stock notification service would -probably serve as a better solution at Best Buy. +Original code still exists, but provides very little utility. A 3rd party stock notification service would probably +serve as a better solution at Best Buy. -~~This is fairly basic right now. Just login to the best buy website in your default browser and then run the command as follows:~~ +~~This is fairly basic right now. Just login to the best buy website in your default browser and then run the command as +follows:~~ ``` python app.py bestbuy --sku [SKU] ``` ~~Example:~~ -```python -python app.py bestbuy --sku 6429440 -``` +``` +python python app.py bestbuy - -sku 6429440 +``` ## Notifications ### Sounds -Local sounds are provided as a means to give you audible cues to what is happening. The notification sound -plays for notable events (e.g., start up, product found for purchase) during the scans. An alarm notification -will play when user interaction is necessary. This is typically when all automated options have been exhausted. -Lastly, a purchase notification sound will play if the bot if successful. These local sounds can be disabled -via the command-line and [tested](#testing-notifications) along with other notification methods + +Local sounds are provided as a means to give you audible cues to what is happening. The notification sound plays for +notable events (e.g., start up, product found for purchase) during the scans. An alarm notification will play when user +interaction is necessary. This is typically when all automated options have been exhausted. Lastly, a purchase +notification sound will play if the bot if successful. These local sounds can be disabled via the command-line +and [tested](#testing-notifications) along with other notification methods ### Apprise -Notifications are now handled by Apprise. Apprise lets you send notifications to a large number of supported notification services. -Check https://github.com/caronc/apprise/wiki for a detailed list. -To enable Apprise notifications, make a copy of `apprise.conf_template` in the `config` directory and name it -`apprise.conf`. Then add apprise formatted urls for your desired notification services as simple text entries -in the config file. Any recognized notification services will be reported on app start. +Notifications are now handled by Apprise. Apprise lets you send notifications to a large number of supported +notification services. Check https://github.com/caronc/apprise/wiki for a detailed list. + +To enable Apprise notifications, make a copy of `apprise.conf_template` in the `config` directory and name it +`apprise.conf`. Then add apprise formatted urls for your desired notification services as simple text entries in the +config file. Any recognized notification services will be reported on app start. + +##### Apprise Example Config: -**Apprise Example Config:** ``` # Hash Tags denote comment lines and blank lines are allowed # Discord (https://github.com/caronc/apprise/wiki/Notify_discord) @@ -293,44 +448,57 @@ https://hooks.slack.com/services/{tokenA}/{tokenB}/{tokenC} ``` -#### Pavlok -To enable shock notifications to your [Pavlok Shockwatch](https://www.amazon.com/Pavlok-PAV2-PERIMETER-BLACK-2/dp/B01N8VJX8P?), -store the url from the pavlok app in the ```pavlok_config.json``` file, you can copy the template from ```pavlok_config.template_json```. +### Pavlok + +To enable shock notifications to +your [Pavlok Shockwatch](https://www.amazon.com/Pavlok-PAV2-PERIMETER-BLACK-2/dp/B01N8VJX8P?), store the url from the +pavlok app in the ```pavlok_config.json``` file, you can copy the template from ```pavlok_config.template_json```. **WARNING:** This feature does not currently support adjusting the intensity, it will always be max (255). + ```json { "base_url": "url goes here" } ``` +### Testing notifications -#### Testing notifications - -Once you have setup your `apprise_config.json ` you can test it by running `python app.py test-notifications` from within your pipenv shell. This will send a test notification to all configured notification services. +Once you have setup your `apprise_config.json ` you can test it by running `python app.py test-notifications` from +within your pipenv shell. This will send a test notification to all configured notification services. ## Troubleshooting -Re-read this documentation. Verify your JSON. +Re-read this documentation. Verify your JSON. + +Consider joining the #tech-support channel in [Discord](https://discord.gg/5tw6UY7g44) for help from the community if +these common fixes don't help. -I suggest joining the #tech-support channel in [Discord](https://discord.gg/qDY2QBtAW6) for help from the community if these common fixes don't help. +**Error: ```selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary```** +The issue is that chrome is not installed in the expected location. +See [Selenium Wiki](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver#requirements) and the section +on [overriding the Chrome binary location .](https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-Using-a-Chrome-executable-in-a-non-standard-location) -**Error: ```selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary```** -The issue is that chrome is not installed in the expected location. See [Selenium Wiki](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver#requirements) and the section on [overriding the Chrome binary location .](https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-Using-a-Chrome-executable-in-a-non-standard-location) +The easy fix for this is to add an option where selenium is used (`selenium_utils.py`) -The easy fix for this is to add an option where selenium is used (`selenium_utils.py``) -```python -chrome_options.binary_location="C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe" +``` +python chrome_options.binary_location = "C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe" ``` -**Error: ```selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 87```** +** +Error: ```selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 87```** -You are not running the proper version of Chrome this requires. As of this update, the current version is Chrome 87. Check your version by going to ```chrome://version/``` in your browser. We are going to be targeting the current stable build of chrome. If you are behind, please update, if you are on a beta or canary branch, you'll have to build your own version of chromedriver-py. +You are not running the proper version of Chrome this requires. As of this update, the current version is Chrome 87. +Check your version by going to ```chrome://version/``` in your browser. We are going to be targeting the current stable +build of chrome. If you are behind, please update, if you are on a beta or canary branch, you'll have to build your own +version of chromedriver-py. ## Raspberry-Pi-Setup + Maybe this works? 1. Prereqs and Setup + ```shell sudo apt update sudo apt upgrade @@ -342,16 +510,20 @@ export PATH=$PATH:/home//.local/bin pipenv shell pipenv install ``` + 2. Leave this Terminal window open. -3. Open the following file in a text editor: +3. Open the following file in a text editor: + ``` /home//.local/share/virtualenvs/fairgame-/lib/python3.7/site-packages/selenium/webdriver/common/service.py ``` + 4. Edit line 38 from `self.path = executable` to `self.path = "chromedriver"`, then save and close the file. 5. Back in Terminal... + ```shell python app.py ``` @@ -360,10 +532,16 @@ python app.py ## Frequently Asked Questions -### 1. Can I run multiple instances of the bot? -Yes. For example you can run one instance to check stock on Best Buy and a separate instance to check stock on Amazon. Bear in mind that if you do this you may end up with multiple purchases going through at the same time. +To keep up with questions, the Discord channel [#FAQ](https://discord.gg/GEsarYKMAw) is where you'll find the latest +answers. If you don't find it there, ask in #tech-support. + +### 1. Can I run multiple instances of the bot? + +Yes. For example you can run one instance to check stock on Best Buy and a separate instance to check stock on Amazon. +Bear in mind that if you do this you may end up with multiple purchases going through at the same time. ### 2. Does Fairgame automatically bypass CAPTCHA's on the store sites? + * For Amazon, yes. The bot will try and auto-solve CAPTCHA's during the checkout process. ## Attribution From 349d938dc4ac737e331821ceb0ac927bbbe8cabd Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 10:33:39 -0500 Subject: [PATCH 012/150] --Updated headless flag --Added headless FAQ --Added Raspberry Pi FAQ --- README.md | 91 +++++++++++++++++------------------------------- stores/amazon.py | 8 +---- 2 files changed, 33 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 39c8c12a..ec2d11ae 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ Usage: app.py amazon option Options: --no-image Do not load images - --headless Unsupported headless mode. GLHF + --headless Runs Chrome in headless mode. --test Run the checkout flow but do not actually purchase the item[s] @@ -469,80 +469,53 @@ within your pipenv shell. This will send a test notification to all configured n ## Troubleshooting -Re-read this documentation. Verify your JSON. ++ Re-read this documentation. -Consider joining the #tech-support channel in [Discord](https://discord.gg/5tw6UY7g44) for help from the community if -these common fixes don't help. ++ Verify your JSON. -**Error: ```selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary```** -The issue is that chrome is not installed in the expected location. -See [Selenium Wiki](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver#requirements) and the section -on [overriding the Chrome binary location .](https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-Using-a-Chrome-executable-in-a-non-standard-location) ++ Consider joining the #tech-support channel in [Discord](https://discord.gg/5tw6UY7g44) for help from the community if + these common fixes don't help. -The easy fix for this is to add an option where selenium is used (`selenium_utils.py`) ++ **Error: ```selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary```** + The issue is that chrome is not installed in the expected location. + See [Selenium Wiki](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver#requirements) and the section + on [overriding the Chrome binary location .](https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-Using-a-Chrome-executable-in-a-non-standard-location) -``` -python chrome_options.binary_location = "C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe" -``` - -** -Error: ```selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 87```** - -You are not running the proper version of Chrome this requires. As of this update, the current version is Chrome 87. -Check your version by going to ```chrome://version/``` in your browser. We are going to be targeting the current stable -build of chrome. If you are behind, please update, if you are on a beta or canary branch, you'll have to build your own -version of chromedriver-py. + The easy fix for this is to add an option where selenium is used (`selenium_utils.py`) -## Raspberry-Pi-Setup + ``` + python chrome_options.binary_location = "C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe" + ``` -Maybe this works? ++ **Error: ```selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 87```** -1. Prereqs and Setup - -```shell -sudo apt update -sudo apt upgrade -sudo apt install chromium-chromedriver -git clone https://github.com/Hari-Nagarajan/fairgame -cd fairgame/ -pip3 install pipenv -export PATH=$PATH:/home//.local/bin -pipenv shell -pipenv install -``` - -2. Leave this Terminal window open. - -3. Open the following file in a text editor: - -``` -/home//.local/share/virtualenvs/fairgame-/lib/python3.7/site-packages/selenium/webdriver/common/service.py -``` - -4. Edit line 38 from `self.path = executable` to `self.path = "chromedriver"`, then save and close the file. - - -5. Back in Terminal... - -```shell -python app.py -``` - -6. Follow [Usage](#Usage) to configure the bot as needed. + You are not running the proper version of Chrome this requires. As of this update, the current version is Chrome 87. + Check your version by going to ```chrome://version/``` in your browser. We are going to be targeting the current stable + build of chrome. If you are behind, please update, if you are on a beta or canary branch, you'll have to build your own + version of chromedriver-py. ## Frequently Asked Questions To keep up with questions, the Discord channel [#FAQ](https://discord.gg/GEsarYKMAw) is where you'll find the latest answers. If you don't find it there, ask in #tech-support. -### 1. Can I run multiple instances of the bot? +1. **Can I run multiple instances of the bot?** + + Yes. For example you can run one instance to check stock on Best Buy and a separate instance to check stock on + Amazon. Bear in mind that if you do this you may end up with multiple purchases going through at the same time. -Yes. For example you can run one instance to check stock on Best Buy and a separate instance to check stock on Amazon. -Bear in mind that if you do this you may end up with multiple purchases going through at the same time. +2. **Does Fairgame automatically bypass CAPTCHA's on the store sites?** + For Amazon, yes. The bot will try and auto-solve CAPTCHA's during the checkout process. -### 2. Does Fairgame automatically bypass CAPTCHA's on the store sites? +3. **Does `--headless` work?** + Yes! A community user identified the issue with the headless option while running on a Raspberry Pi. This allowed + the developers to update the codebase to consistently work correctly on headless server environments. Give it a try + and let us know if you have any issues. -* For Amazon, yes. The bot will try and auto-solve CAPTCHA's during the checkout process. +4. **Does Fairgame run on a Raspberry Pi?** + Yes, with caveats. Most people seem to have success with Raspberry Pi 4. The 2 GB model may need to run the headless + option due to the smaller memory footprint. Still awaiting community feedback on running on a Pi 3. CPU and memory + capacity seem to be the limiting factor for older Pi models. ## Attribution diff --git a/stores/amazon.py b/stores/amazon.py index 36c59761..70b260f6 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1161,7 +1161,7 @@ def show_config(self): ) log.info(f"--Delay of {self.refresh_delay} seconds") if self.headless: - log.info(f"--Headless doesn't work!") + log.info(f"--Browser running in headless mode") if self.used: log.info(f"--Used items are considered for purchase") if self.checkshipping: @@ -1178,8 +1178,6 @@ def show_config(self): log.info(f"--Detailed screenshots/notifications is enabled") if self.log_stock_check: log.info(f"--Additional stock check logging enabled") - if self.testing: - log.warning(f"--Testing Mode. NO Purchases will be made.") if self.slow_mode: log.warning(f"--Slow-mode enabled. Pages will fully load before execution.") if self.shipping_bypass: @@ -1202,10 +1200,6 @@ def show_config(self): log.info(f"--No images will be requested") if not self.notification_handler.sound_enabled: log.info(f"--Notification sounds are disabled.") - if self.headless: - log.warning( - f"--Running headless is unsupported. If you get it to work, please let us know on Discord." - ) if self.testing: log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") From ca10e9b1cbe29726eeaf3bae6d674293e14fa944 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 14:27:57 -0500 Subject: [PATCH 013/150] -- Removed reference to older version of Python on Pi --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index ec2d11ae..d8884862 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Edit the newly created files with your settings based on your [configuration](#c ### Installation Raspberry Pi 4 (2 GB+) -This is an abridged version of the community created document +This is an abridged version of the community created document by UnidentifiedWarlock and Judarius. It can be found [here](https://docs.google.com/document/d/1VUxXhATZ8sZOJxdh3AIY6OGqwLRmrAcPikKZAwphIE8/edit). If the steps here don't work on your Pi 4, look there for additional options. This hasn't been tested on a Pi 3, but given enough RAM to run Chrome, it may very well work. Let us know. @@ -174,8 +174,6 @@ Then save and close the file. Additional steps outside of the readme: -uncomment requires 3.8 Piplock, as default pi python 2.7.16 works fine - If you get a compiler error when doing pipenv install run: ```shell From d448cc886220818d204d0ac9f8e8dd4fc94c65f7 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+dakkjaniels@users.noreply.github.com> Date: Tue, 5 Jan 2021 14:47:48 -0500 Subject: [PATCH 014/150] -- Explicitly declared sel_exceptions.WebDriverException in cases where a general error has occured with the web driver. --- stores/amazon.py | 243 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 182 insertions(+), 61 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 36c59761..c36613d7 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1,17 +1,18 @@ -import fileinput import json import math import os import platform +import random import time from datetime import datetime +import fileinput import psutil +import stdiomask from amazoncaptcha import AmazonCaptcha from chromedriver_py import binary_path # this will get you the path variable from furl import furl from price_parser import parse_price -from pypresence import exceptions as pyexceptions from selenium import webdriver from selenium.common import exceptions as sel_exceptions from selenium.webdriver.common.keys import Keys @@ -19,6 +20,7 @@ from utils import discord_presence as presence from utils.debugger import debug +from utils.encryption import create_encrypted_config, load_encrypted_config from utils.logger import log from utils.selenium_utils import options, enable_headless @@ -30,6 +32,132 @@ CHECKOUT_URL = "https://{domain}/gp/cart/desktop/go-to-checkout.html/ref=ox_sc_proceed?partialCheckoutCart=1&isToBeGiftWrappedBefore=0&proceedToRetailCheckout=Proceed+to+checkout&proceedToCheckout=1&cartInitiateId={cart_id}" AUTOBUY_CONFIG_PATH = "config/amazon_config.json" +CREDENTIAL_FILE = "config/amazon_credentials.json" + +SIGN_IN_TEXT = [ + "Hello, Sign in", + "Sign in", + "Hola, Identifícate", + "Bonjour, Identifiez-vous", + "Ciao, Accedi", + "Hallo, Anmelden", + "Hallo, Inloggen", +] +SIGN_IN_TITLES = [ + "Amazon Sign In", + "Amazon Sign-In", + "Amazon Anmelden", + "Iniciar sesión en Amazon", + "Connexion Amazon", + "Amazon Accedi", + "Inloggen bij Amazon", +] +CAPTCHA_PAGE_TITLES = ["Robot Check"] +HOME_PAGE_TITLES = [ + "Amazon.com: Online Shopping for Electronics, Apparel, Computers, Books, DVDs & more", + "AmazonSmile: You shop. Amazon gives.", + "Amazon.ca: Low Prices – Fast Shipping – Millions of Items", + "Amazon.co.uk: Low Prices in Electronics, Books, Sports Equipment & more", + "Amazon.de: Low Prices in Electronics, Books, Sports Equipment & more", + "Amazon.de: Günstige Preise für Elektronik & Foto, Filme, Musik, Bücher, Games, Spielzeug & mehr", + "Amazon.es: compra online de electrónica, libros, deporte, hogar, moda y mucho más.", + "Amazon.de: Günstige Preise für Elektronik & Foto, Filme, Musik, Bücher, Games, Spielzeug & mehr", + "Amazon.fr : livres, DVD, jeux vidéo, musique, high-tech, informatique, jouets, vêtements, chaussures, sport, bricolage, maison, beauté, puériculture, épicerie et plus encore !", + "Amazon.it: elettronica, libri, musica, fashion, videogiochi, DVD e tanto altro", + "Amazon.nl: Groot aanbod, kleine prijzen in o.a. Elektronica, boeken, sport en meer", # this site doesn't work anymore + "Amazon.se: Låga priser på Elektronik, Böcker, Sportutrustning & mer", # this site doesn't work anymore +] +SHOPING_CART_TITLES = [ + "Amazon.com Shopping Cart", + "Amazon.ca Shopping Cart", + "Amazon.co.uk Shopping Basket", + "Amazon.de Basket", + "Amazon.de Einkaufswagen", + "AmazonSmile Einkaufswagen", + "Cesta de compra Amazon.es", + "Amazon.fr Panier", + "Carrello Amazon.it", + "AmazonSmile Shopping Cart", + "AmazonSmile Shopping Basket", + "Amazon.nl-winkelwagen", +] +CHECKOUT_TITLES = [ + "Amazon.com Checkout", + "Amazon.co.uk Checkout", + "Place Your Order - Amazon.ca Checkout", + "Place Your Order - Amazon.co.uk Checkout", + "Amazon.de Checkout", + "Place Your Order - Amazon.de Checkout", + "Amazon.de - Bezahlvorgang", + "Bestellung aufgeben - Amazon.de-Bezahlvorgang", + "Place Your Order - Amazon.com Checkout", + "Place Your Order - Amazon.com", + "Tramitar pedido en Amazon.es", + "Processus de paiement Amazon.com", + "Confirmar pedido - Compra Amazon.es", + "Passez votre commande - Processus de paiement Amazon.fr", + "Ordina - Cassa Amazon.it", + "AmazonSmile Checkout", + "Plaats je bestelling - Amazon.nl-kassa", + "Place Your Order - AmazonSmile Checkout", + "Preparing your order", + "Ihre Bestellung wird vorbereitet", +] +ORDER_COMPLETE_TITLES = [ + "Amazon.com Thanks You", + "Amazon.ca Thanks You", + "AmazonSmile Thanks You", + "Thank you", + "Amazon.fr Merci", + "Merci", + "Amazon.es te da las gracias", + "Amazon.fr vous remercie.", + "Grazie da Amazon.it", + "Hartelijk dank", + "Thank You", + "Amazon.de Vielen Dank", +] +ADD_TO_CART_TITLES = [ + "Amazon.com: Please Confirm Your Action", + "Amazon.de: Bitte bestätigen Sie Ihre Aktion", + "Amazon.de: Please Confirm Your Action", + "Amazon.es: confirma tu acción", + "Amazon.com : Veuillez confirmer votre action", # Careful, required non-breaking space after .com ( ) + "Amazon.it: confermare l'operazione", + "AmazonSmile: Please Confirm Your Action", + "", # Amazon.nl has en empty title, sigh. +] +BUSINESS_PO_TITLES = [ + "Business order information", +] + +DOGGO_TITLES = ["Sorry! Something went wrong!"] + +# this is not non-US friendly +SHIPPING_ONLY_IF = "FREE Shipping on orders over" + +TWOFA_TITLES = ["Two-Step Verification"] + +PRIME_TITLES = ["Complete your Amazon Prime sign up"] + +OUT_OF_STOCK = ["Out of Stock - AmazonSmile Checkout"] + +NO_SELLERS = [ + "Currently, there are no sellers that can deliver this item to your location.", + "There are currently no listings for this search. Try a different refinement.", + "There are currently no listings for this product in . Try changing the condition type.", + "Actualmente, no hay listas para este producto en . Intenta cambiar el tipo de condición.", + "Derzeit gibt es keine Verkäufer, die diesen Artikel an Ihren Standort liefern können.", + "Actualmente, no hay vendedores que puedan entregar este producto en tu ubicación.", + "Il n’y a actuellement aucun vendeur en mesure de livrer ce produit sur votre zone géographique.", + "Il n'y a actuellement pas de produits répondant à ces critères. Essayez de changer les filtres.", + "No existen listados para esta búsqueda. Probar con otro filtro.", + "In gibt es derzeit keine Listungen für dieses Produkt. Versuchen Sie, den Zustandstyp zu ändern.", + "Al momento, non ci sono seller in grado di spedire questo articolo alla tua sede.", + "Al momento non ci sono offerte per questo prodotto in . Prova a modificare il tipo di condizione.", +] + +# OFFER_PAGE_TITLES = ["Amazon.com: Buying Choices:"] BUTTON_XPATHS = [ '//*[@id="submitOrderButtonId"]/span/input', @@ -59,8 +187,6 @@ DEFAULT_MAX_TIMEOUT = 10 DEFAULT_MAX_URL_FAIL = 5 -amazon_config = None - class Amazon: def __init__( @@ -74,8 +200,8 @@ def __init__( no_screenshots=False, disable_presence=False, slow_mode=False, - no_image=False, encryption_pass=None, + no_image=False, log_stock_check=False, shipping_bypass=False, ): @@ -103,17 +229,7 @@ def __init__( self.shipping_bypass = shipping_bypass presence.enabled = not disable_presence - - global amazon_config - from cli.cli import global_config - - amazon_config = global_config.get_amazon_config(encryption_pass) - - try: - presence.start_presence() - except Exception in pyexceptions: - log.error("Discord presence failed to load") - presence.enabled = False + presence.start_presence() # Create necessary sub-directories if they don't exist if not os.path.exists("screenshots"): @@ -128,6 +244,18 @@ def __init__( except: raise + if os.path.exists(CREDENTIAL_FILE): + credential = load_encrypted_config(CREDENTIAL_FILE, encryption_pass) + self.username = credential["username"] + self.password = credential["password"] + else: + log.info("No credential file found, let's make one") + log.info("NOTE: DO NOT SAVE YOUR CREDENTIALS IN CHROME, CLICK NEVER!") + credential = self.await_credential_input() + create_encrypted_config(credential, CREDENTIAL_FILE) + self.username = credential["username"] + self.password = credential["password"] + if os.path.exists(AUTOBUY_CONFIG_PATH): with open(AUTOBUY_CONFIG_PATH) as json_file: try: @@ -159,6 +287,15 @@ def __init__( for key in AMAZON_URLS.keys(): AMAZON_URLS[key] = AMAZON_URLS[key].format(domain=self.amazon_website) + @staticmethod + def await_credential_input(): + username = input("Amazon login ID: ") + password = stdiomask.getpass(prompt="Amazon Password: ") + return { + "username": username, + "password": password, + } + def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): self.testing = test self.refresh_delay = delay @@ -169,7 +306,7 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): try: self.get_page(url=AMAZON_URLS["BASE_URL"]) break - except sel_exceptions: + except sel_exceptions.WebDriverException: log.error( "Couldn't talk to " + AMAZON_URLS["BASE_URL"] @@ -278,9 +415,8 @@ def handle_startup(self): def is_logged_in(self): try: text = self.driver.find_element_by_id("nav-link-accountList").text - return not any(sign_in in text for sign_in in amazon_config["SIGN_IN_TEXT"]) + return not any(sign_in in text for sign_in in SIGN_IN_TEXT) except sel_exceptions.NoSuchElementException: - return False @debug @@ -306,7 +442,7 @@ def login(self): if email_field: try: - email_field.send_keys(amazon_config["username"] + Keys.RETURN) + email_field.send_keys(self.username + Keys.RETURN) except sel_exceptions.ElementNotInteractableException: log.info("Email not needed.") else: @@ -342,7 +478,7 @@ def login(self): captcha_entry = [] if password_field: - password_field.send_keys(amazon_config["password"]) + password_field.send_keys(self.password) # check for captcha try: captcha_entry = self.driver.find_element_by_xpath( @@ -378,11 +514,11 @@ def login(self): self.driver.refresh() time.sleep(5) - if self.driver.title in amazon_config["TWOFA_TITLES"]: + if self.driver.title in TWOFA_TITLES: log.info("enter in your two-step verification code in browser") - while self.driver.title in amazon_config["WOFA_TITLES"]: + while self.driver.title in TWOFA_TITLES: time.sleep(0.2) - log.info(f'Logged in as {amazon_config["username"]}') + log.info(f"Logged in as {self.username}") @debug def run_asins(self, delay): @@ -477,7 +613,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): except sel_exceptions.NoSuchElementException: pass - if test and (test.text in amazon_config["NO_SELLERS"]): + if test and (test.text in NO_SELLERS): return False if time.time() > timeout: log.info(f"failed to load page for {asin}, going to next ASIN") @@ -516,7 +652,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False try: if self.checkshipping: - if amazon_config["SHIPPING_ONLY_IF"] in shipping[idx].text: + if SHIPPING_ONLY_IF in shipping[idx].text: ship_price = parse_price("0") else: ship_price = parse_price(shipping[idx].text) @@ -559,7 +695,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False self.wait_for_page_change(current_title) # log.info(f"page title is {self.driver.title}") - if self.driver.title in amazon_config["SHOPPING_CART_TITLES"]: + if self.driver.title in SHOPING_CART_TITLES: return True else: log.info("did not add to cart, trying again") @@ -608,26 +744,26 @@ def navigate_pages(self, test): if time.time() > timeout: log.debug("Time out reached, page title was still blank.") break - if title in amazon_config["SIGN_IN_TITLES"]: + if title in SIGN_IN_TITLES: self.login() - elif title in amazon_config["CAPTCHA_PAGE_TITLES"]: + elif title in CAPTCHA_PAGE_TITLES: self.handle_captcha() - elif title in amazon_config["SHOPPING_CART_TITLES"]: + elif title in SHOPING_CART_TITLES: self.handle_cart() - elif title in amazon_config["CHECKOUT_TITLES"]: + elif title in CHECKOUT_TITLES: self.handle_checkout(test) - elif title in amazon_config["ORDER_COMPLETE_TITLES"]: + elif title in ORDER_COMPLETE_TITLES: self.handle_order_complete() - elif title in amazon_config["PRIME_TITLES"]: + elif title in PRIME_TITLES: self.handle_prime_signup() - elif title in amazon_config["HOME_PAGE_TITLES"]: + elif title in HOME_PAGE_TITLES: # if home page, something went wrong self.handle_home_page() - elif title in amazon_config["DOGGO_TITLES"]: + elif title in DOGGO_TITLES: self.handle_doggos() - elif title in amazon_config["OUT_OF_STOCK"]: + elif title in OUT_OF_STOCK: self.handle_out_of_stock() - elif title in amazon_config["BUSINESS_PO_TITLES"]: + elif title in BUSINESS_PO_TITLES: self.handle_business_po() else: log.debug(f"title is: [{title}]") @@ -713,7 +849,7 @@ def navigate_pages(self, test): log.info("Clicked button.") self.wait_for_page_change(page_title=title) return - except sel_exceptions: + except sel_exceptions.WebDriverException: log.error("Could not click ship to address button") if self.get_cart_count() == 0: @@ -741,7 +877,7 @@ def navigate_pages(self, test): log.info("going to try and redirect to cart page") try: self.driver.get(AMAZON_URLS["CART_URL"]) - except sel_exceptions: + except sel_exceptions.WebDriverException: log.error( "failed to load cart URL, refreshing and returning to handler" ) @@ -784,7 +920,7 @@ def navigate_pages(self, test): button.click() log.info("Clicked ptc button") self.wait_for_page_change(page_title=current_title) - except sel_exceptions: + except sel_exceptions.WebDriverException: log.info( "Could not click button - refreshing and returning to checkout handler" ) @@ -838,7 +974,7 @@ def handle_prime_signup(self): "Prime offer page popped up, user intervention required" ) timeout = self.get_timeout(timeout=60) - while self.driver.title in amazon_config["PRIME_TITLES"]: + while self.driver.title in PRIME_TITLES: if time.time() > timeout: log.info( "user did not intervene in time, will try and refresh page" @@ -931,7 +1067,7 @@ def handle_cart(self): button.click() log.info("Clicked Proceed to Checkout Button") self.wait_for_page_change(page_title=current_page) - except sel_exceptions: + except sel_exceptions.WebDriverException: log.error("Problem clicking Proceed to Checkout button.") log.info("Refreshing page to try again") self.driver.refresh() @@ -1122,7 +1258,7 @@ def get_webdriver_pids(self): self.webdriver_child_pids.append(child.pid) def get_page(self, url): - check_cart_element = None + check_cart_element = [] current_page = [] try: check_cart_element = self.driver.find_element_by_xpath( @@ -1156,9 +1292,7 @@ def __del__(self): def show_config(self): log.info(f"{'=' * 50}") - log.info( - f"Starting Amazon ASIN Hunt on {AMAZON_URLS['BASE_URL']} for {len(self.asin_list)} Products with:" - ) + log.info(f"Starting Amazon ASIN Hunt for {len(self.asin_list)} Products with:") log.info(f"--Delay of {self.refresh_delay} seconds") if self.headless: log.info(f"--Headless doesn't work!") @@ -1169,7 +1303,7 @@ def show_config(self): else: log.info(f"--Free Shipping items only") if self.single_shot: - log.info("--Single Shot purchase enabled") + log.info("\tSingle Shot purchase enabled") if not self.take_screenshots: log.info( f"--Screenshotting is Disabled, DO NOT ASK FOR HELP IN TECH SUPPORT IF YOU HAVE NO SCREENSHOTS!" @@ -1198,16 +1332,6 @@ def show_config(self): log.info( f"--Looking for {len(asins)} ASINs between {self.reserve_min[idx]:.2f} and {self.reserve_max[idx]:.2f}" ) - if self.no_image: - log.info(f"--No images will be requested") - if not self.notification_handler.sound_enabled: - log.info(f"--Notification sounds are disabled.") - if self.headless: - log.warning( - f"--Running headless is unsupported. If you get it to work, please let us know on Discord." - ) - if self.testing: - log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") def create_driver(self): @@ -1229,10 +1353,7 @@ def create_driver(self): else: prefs["profile.managed_default_content_settings.images"] = 0 options.add_experimental_option("prefs", prefs) - path_to_profile = os.path.join( - os.path.dirname(os.path.abspath("__file__")), ".profile-amz" - ) - options.add_argument(f"user-data-dir={path_to_profile}") + options.add_argument(f"user-data-dir=.profile-amz") if not self.slow_mode: options.set_capability("pageLoadStrategy", "none") From 038747ed81ff2656ae51e00f2f90e87ecc9eaba6 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 16:34:32 -0500 Subject: [PATCH 015/150] -- Corrected previous commit to explicitly declared sel_exceptions.WebDriverException in cases where a general error has occured with the web driver. --- stores/amazon.py | 233 ++++++++++++----------------------------------- 1 file changed, 56 insertions(+), 177 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index c36613d7..0dd5a4d3 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1,18 +1,17 @@ +import fileinput import json import math import os import platform -import random import time from datetime import datetime -import fileinput import psutil -import stdiomask from amazoncaptcha import AmazonCaptcha from chromedriver_py import binary_path # this will get you the path variable from furl import furl from price_parser import parse_price +from pypresence import exceptions as pyexceptions from selenium import webdriver from selenium.common import exceptions as sel_exceptions from selenium.webdriver.common.keys import Keys @@ -20,7 +19,6 @@ from utils import discord_presence as presence from utils.debugger import debug -from utils.encryption import create_encrypted_config, load_encrypted_config from utils.logger import log from utils.selenium_utils import options, enable_headless @@ -32,132 +30,6 @@ CHECKOUT_URL = "https://{domain}/gp/cart/desktop/go-to-checkout.html/ref=ox_sc_proceed?partialCheckoutCart=1&isToBeGiftWrappedBefore=0&proceedToRetailCheckout=Proceed+to+checkout&proceedToCheckout=1&cartInitiateId={cart_id}" AUTOBUY_CONFIG_PATH = "config/amazon_config.json" -CREDENTIAL_FILE = "config/amazon_credentials.json" - -SIGN_IN_TEXT = [ - "Hello, Sign in", - "Sign in", - "Hola, Identifícate", - "Bonjour, Identifiez-vous", - "Ciao, Accedi", - "Hallo, Anmelden", - "Hallo, Inloggen", -] -SIGN_IN_TITLES = [ - "Amazon Sign In", - "Amazon Sign-In", - "Amazon Anmelden", - "Iniciar sesión en Amazon", - "Connexion Amazon", - "Amazon Accedi", - "Inloggen bij Amazon", -] -CAPTCHA_PAGE_TITLES = ["Robot Check"] -HOME_PAGE_TITLES = [ - "Amazon.com: Online Shopping for Electronics, Apparel, Computers, Books, DVDs & more", - "AmazonSmile: You shop. Amazon gives.", - "Amazon.ca: Low Prices – Fast Shipping – Millions of Items", - "Amazon.co.uk: Low Prices in Electronics, Books, Sports Equipment & more", - "Amazon.de: Low Prices in Electronics, Books, Sports Equipment & more", - "Amazon.de: Günstige Preise für Elektronik & Foto, Filme, Musik, Bücher, Games, Spielzeug & mehr", - "Amazon.es: compra online de electrónica, libros, deporte, hogar, moda y mucho más.", - "Amazon.de: Günstige Preise für Elektronik & Foto, Filme, Musik, Bücher, Games, Spielzeug & mehr", - "Amazon.fr : livres, DVD, jeux vidéo, musique, high-tech, informatique, jouets, vêtements, chaussures, sport, bricolage, maison, beauté, puériculture, épicerie et plus encore !", - "Amazon.it: elettronica, libri, musica, fashion, videogiochi, DVD e tanto altro", - "Amazon.nl: Groot aanbod, kleine prijzen in o.a. Elektronica, boeken, sport en meer", # this site doesn't work anymore - "Amazon.se: Låga priser på Elektronik, Böcker, Sportutrustning & mer", # this site doesn't work anymore -] -SHOPING_CART_TITLES = [ - "Amazon.com Shopping Cart", - "Amazon.ca Shopping Cart", - "Amazon.co.uk Shopping Basket", - "Amazon.de Basket", - "Amazon.de Einkaufswagen", - "AmazonSmile Einkaufswagen", - "Cesta de compra Amazon.es", - "Amazon.fr Panier", - "Carrello Amazon.it", - "AmazonSmile Shopping Cart", - "AmazonSmile Shopping Basket", - "Amazon.nl-winkelwagen", -] -CHECKOUT_TITLES = [ - "Amazon.com Checkout", - "Amazon.co.uk Checkout", - "Place Your Order - Amazon.ca Checkout", - "Place Your Order - Amazon.co.uk Checkout", - "Amazon.de Checkout", - "Place Your Order - Amazon.de Checkout", - "Amazon.de - Bezahlvorgang", - "Bestellung aufgeben - Amazon.de-Bezahlvorgang", - "Place Your Order - Amazon.com Checkout", - "Place Your Order - Amazon.com", - "Tramitar pedido en Amazon.es", - "Processus de paiement Amazon.com", - "Confirmar pedido - Compra Amazon.es", - "Passez votre commande - Processus de paiement Amazon.fr", - "Ordina - Cassa Amazon.it", - "AmazonSmile Checkout", - "Plaats je bestelling - Amazon.nl-kassa", - "Place Your Order - AmazonSmile Checkout", - "Preparing your order", - "Ihre Bestellung wird vorbereitet", -] -ORDER_COMPLETE_TITLES = [ - "Amazon.com Thanks You", - "Amazon.ca Thanks You", - "AmazonSmile Thanks You", - "Thank you", - "Amazon.fr Merci", - "Merci", - "Amazon.es te da las gracias", - "Amazon.fr vous remercie.", - "Grazie da Amazon.it", - "Hartelijk dank", - "Thank You", - "Amazon.de Vielen Dank", -] -ADD_TO_CART_TITLES = [ - "Amazon.com: Please Confirm Your Action", - "Amazon.de: Bitte bestätigen Sie Ihre Aktion", - "Amazon.de: Please Confirm Your Action", - "Amazon.es: confirma tu acción", - "Amazon.com : Veuillez confirmer votre action", # Careful, required non-breaking space after .com ( ) - "Amazon.it: confermare l'operazione", - "AmazonSmile: Please Confirm Your Action", - "", # Amazon.nl has en empty title, sigh. -] -BUSINESS_PO_TITLES = [ - "Business order information", -] - -DOGGO_TITLES = ["Sorry! Something went wrong!"] - -# this is not non-US friendly -SHIPPING_ONLY_IF = "FREE Shipping on orders over" - -TWOFA_TITLES = ["Two-Step Verification"] - -PRIME_TITLES = ["Complete your Amazon Prime sign up"] - -OUT_OF_STOCK = ["Out of Stock - AmazonSmile Checkout"] - -NO_SELLERS = [ - "Currently, there are no sellers that can deliver this item to your location.", - "There are currently no listings for this search. Try a different refinement.", - "There are currently no listings for this product in . Try changing the condition type.", - "Actualmente, no hay listas para este producto en . Intenta cambiar el tipo de condición.", - "Derzeit gibt es keine Verkäufer, die diesen Artikel an Ihren Standort liefern können.", - "Actualmente, no hay vendedores que puedan entregar este producto en tu ubicación.", - "Il n’y a actuellement aucun vendeur en mesure de livrer ce produit sur votre zone géographique.", - "Il n'y a actuellement pas de produits répondant à ces critères. Essayez de changer les filtres.", - "No existen listados para esta búsqueda. Probar con otro filtro.", - "In gibt es derzeit keine Listungen für dieses Produkt. Versuchen Sie, den Zustandstyp zu ändern.", - "Al momento, non ci sono seller in grado di spedire questo articolo alla tua sede.", - "Al momento non ci sono offerte per questo prodotto in . Prova a modificare il tipo di condizione.", -] - -# OFFER_PAGE_TITLES = ["Amazon.com: Buying Choices:"] BUTTON_XPATHS = [ '//*[@id="submitOrderButtonId"]/span/input', @@ -187,6 +59,8 @@ DEFAULT_MAX_TIMEOUT = 10 DEFAULT_MAX_URL_FAIL = 5 +amazon_config = None + class Amazon: def __init__( @@ -200,8 +74,8 @@ def __init__( no_screenshots=False, disable_presence=False, slow_mode=False, - encryption_pass=None, no_image=False, + encryption_pass=None, log_stock_check=False, shipping_bypass=False, ): @@ -229,7 +103,17 @@ def __init__( self.shipping_bypass = shipping_bypass presence.enabled = not disable_presence - presence.start_presence() + + global amazon_config + from cli.cli import global_config + + amazon_config = global_config.get_amazon_config(encryption_pass) + + try: + presence.start_presence() + except Exception in pyexceptions: + log.error("Discord presence failed to load") + presence.enabled = False # Create necessary sub-directories if they don't exist if not os.path.exists("screenshots"): @@ -244,18 +128,6 @@ def __init__( except: raise - if os.path.exists(CREDENTIAL_FILE): - credential = load_encrypted_config(CREDENTIAL_FILE, encryption_pass) - self.username = credential["username"] - self.password = credential["password"] - else: - log.info("No credential file found, let's make one") - log.info("NOTE: DO NOT SAVE YOUR CREDENTIALS IN CHROME, CLICK NEVER!") - credential = self.await_credential_input() - create_encrypted_config(credential, CREDENTIAL_FILE) - self.username = credential["username"] - self.password = credential["password"] - if os.path.exists(AUTOBUY_CONFIG_PATH): with open(AUTOBUY_CONFIG_PATH) as json_file: try: @@ -287,15 +159,6 @@ def __init__( for key in AMAZON_URLS.keys(): AMAZON_URLS[key] = AMAZON_URLS[key].format(domain=self.amazon_website) - @staticmethod - def await_credential_input(): - username = input("Amazon login ID: ") - password = stdiomask.getpass(prompt="Amazon Password: ") - return { - "username": username, - "password": password, - } - def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): self.testing = test self.refresh_delay = delay @@ -415,8 +278,9 @@ def handle_startup(self): def is_logged_in(self): try: text = self.driver.find_element_by_id("nav-link-accountList").text - return not any(sign_in in text for sign_in in SIGN_IN_TEXT) + return not any(sign_in in text for sign_in in amazon_config["SIGN_IN_TEXT"]) except sel_exceptions.NoSuchElementException: + return False @debug @@ -442,7 +306,7 @@ def login(self): if email_field: try: - email_field.send_keys(self.username + Keys.RETURN) + email_field.send_keys(amazon_config["username"] + Keys.RETURN) except sel_exceptions.ElementNotInteractableException: log.info("Email not needed.") else: @@ -478,7 +342,7 @@ def login(self): captcha_entry = [] if password_field: - password_field.send_keys(self.password) + password_field.send_keys(amazon_config["password"]) # check for captcha try: captcha_entry = self.driver.find_element_by_xpath( @@ -514,11 +378,11 @@ def login(self): self.driver.refresh() time.sleep(5) - if self.driver.title in TWOFA_TITLES: + if self.driver.title in amazon_config["TWOFA_TITLES"]: log.info("enter in your two-step verification code in browser") - while self.driver.title in TWOFA_TITLES: + while self.driver.title in amazon_config["WOFA_TITLES"]: time.sleep(0.2) - log.info(f"Logged in as {self.username}") + log.info(f'Logged in as {amazon_config["username"]}') @debug def run_asins(self, delay): @@ -613,7 +477,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): except sel_exceptions.NoSuchElementException: pass - if test and (test.text in NO_SELLERS): + if test and (test.text in amazon_config["NO_SELLERS"]): return False if time.time() > timeout: log.info(f"failed to load page for {asin}, going to next ASIN") @@ -652,7 +516,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False try: if self.checkshipping: - if SHIPPING_ONLY_IF in shipping[idx].text: + if amazon_config["SHIPPING_ONLY_IF"] in shipping[idx].text: ship_price = parse_price("0") else: ship_price = parse_price(shipping[idx].text) @@ -695,7 +559,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False self.wait_for_page_change(current_title) # log.info(f"page title is {self.driver.title}") - if self.driver.title in SHOPING_CART_TITLES: + if self.driver.title in amazon_config["SHOPPING_CART_TITLES"]: return True else: log.info("did not add to cart, trying again") @@ -744,26 +608,26 @@ def navigate_pages(self, test): if time.time() > timeout: log.debug("Time out reached, page title was still blank.") break - if title in SIGN_IN_TITLES: + if title in amazon_config["SIGN_IN_TITLES"]: self.login() - elif title in CAPTCHA_PAGE_TITLES: + elif title in amazon_config["CAPTCHA_PAGE_TITLES"]: self.handle_captcha() - elif title in SHOPING_CART_TITLES: + elif title in amazon_config["SHOPPING_CART_TITLES"]: self.handle_cart() - elif title in CHECKOUT_TITLES: + elif title in amazon_config["CHECKOUT_TITLES"]: self.handle_checkout(test) - elif title in ORDER_COMPLETE_TITLES: + elif title in amazon_config["ORDER_COMPLETE_TITLES"]: self.handle_order_complete() - elif title in PRIME_TITLES: + elif title in amazon_config["PRIME_TITLES"]: self.handle_prime_signup() - elif title in HOME_PAGE_TITLES: + elif title in amazon_config["HOME_PAGE_TITLES"]: # if home page, something went wrong self.handle_home_page() - elif title in DOGGO_TITLES: + elif title in amazon_config["DOGGO_TITLES"]: self.handle_doggos() - elif title in OUT_OF_STOCK: + elif title in amazon_config["OUT_OF_STOCK"]: self.handle_out_of_stock() - elif title in BUSINESS_PO_TITLES: + elif title in amazon_config["BUSINESS_PO_TITLES"]: self.handle_business_po() else: log.debug(f"title is: [{title}]") @@ -974,7 +838,7 @@ def handle_prime_signup(self): "Prime offer page popped up, user intervention required" ) timeout = self.get_timeout(timeout=60) - while self.driver.title in PRIME_TITLES: + while self.driver.title in amazon_config["PRIME_TITLES"]: if time.time() > timeout: log.info( "user did not intervene in time, will try and refresh page" @@ -1258,7 +1122,7 @@ def get_webdriver_pids(self): self.webdriver_child_pids.append(child.pid) def get_page(self, url): - check_cart_element = [] + check_cart_element = None current_page = [] try: check_cart_element = self.driver.find_element_by_xpath( @@ -1292,7 +1156,9 @@ def __del__(self): def show_config(self): log.info(f"{'=' * 50}") - log.info(f"Starting Amazon ASIN Hunt for {len(self.asin_list)} Products with:") + log.info( + f"Starting Amazon ASIN Hunt on {AMAZON_URLS['BASE_URL']} for {len(self.asin_list)} Products with:" + ) log.info(f"--Delay of {self.refresh_delay} seconds") if self.headless: log.info(f"--Headless doesn't work!") @@ -1303,7 +1169,7 @@ def show_config(self): else: log.info(f"--Free Shipping items only") if self.single_shot: - log.info("\tSingle Shot purchase enabled") + log.info("--Single Shot purchase enabled") if not self.take_screenshots: log.info( f"--Screenshotting is Disabled, DO NOT ASK FOR HELP IN TECH SUPPORT IF YOU HAVE NO SCREENSHOTS!" @@ -1332,6 +1198,16 @@ def show_config(self): log.info( f"--Looking for {len(asins)} ASINs between {self.reserve_min[idx]:.2f} and {self.reserve_max[idx]:.2f}" ) + if self.no_image: + log.info(f"--No images will be requested") + if not self.notification_handler.sound_enabled: + log.info(f"--Notification sounds are disabled.") + if self.headless: + log.warning( + f"--Running headless is unsupported. If you get it to work, please let us know on Discord." + ) + if self.testing: + log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") def create_driver(self): @@ -1353,7 +1229,10 @@ def create_driver(self): else: prefs["profile.managed_default_content_settings.images"] = 0 options.add_experimental_option("prefs", prefs) - options.add_argument(f"user-data-dir=.profile-amz") + path_to_profile = os.path.join( + os.path.dirname(os.path.abspath("__file__")), ".profile-amz" + ) + options.add_argument(f"user-data-dir={path_to_profile}") if not self.slow_mode: options.set_capability("pageLoadStrategy", "none") From 40dee1c98c85db4e7947ef9614159920453d9e41 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 16:42:13 -0500 Subject: [PATCH 016/150] -- Added missing dependencies --- Pipfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 3179fbb6..f3abd491 100644 --- a/Pipfile +++ b/Pipfile @@ -21,7 +21,7 @@ slackclient = "*" playsound = "*" prompt_toolkit = "*" aiohttp = "*" -pyobjc = {version = "*", sys_platform = "== 'darwin'"} +pyobjc = { version = "*", sys_platform = "== 'darwin'" } async-timeout = "*" amazoncaptcha = "==0.4.4" browser-cookie3 = "*" @@ -29,9 +29,12 @@ coloredlogs = "*" apprise = "*" price-parser = "*" pypresence = "==4.0.0" -pywin32 = {version = "*", sys_platform = "== 'win32'"} +pywin32 = { version = "*", sys_platform = "== 'win32'" } psutil = "*" stdiomask = "*" +packaging = "*" +config = "*" +lxml = "*" [requires] python_version = "3.8" From d07eabc71171aa65f1e9fc82080aa0dae142bc58 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+dakkjaniels@users.noreply.github.com> Date: Tue, 5 Jan 2021 14:47:48 -0500 Subject: [PATCH 017/150] -- Explicitly declared sel_exceptions.WebDriverException in cases where a general error has occured with the web driver. --- stores/amazon.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 70b260f6..ea7959d7 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -169,7 +169,7 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): try: self.get_page(url=AMAZON_URLS["BASE_URL"]) break - except sel_exceptions: + except sel_exceptions.WebDriverException: log.error( "Couldn't talk to " + AMAZON_URLS["BASE_URL"] @@ -713,7 +713,7 @@ def navigate_pages(self, test): log.info("Clicked button.") self.wait_for_page_change(page_title=title) return - except sel_exceptions: + except sel_exceptions.WebDriverException: log.error("Could not click ship to address button") if self.get_cart_count() == 0: @@ -741,7 +741,7 @@ def navigate_pages(self, test): log.info("going to try and redirect to cart page") try: self.driver.get(AMAZON_URLS["CART_URL"]) - except sel_exceptions: + except sel_exceptions.WebDriverException: log.error( "failed to load cart URL, refreshing and returning to handler" ) @@ -784,7 +784,7 @@ def navigate_pages(self, test): button.click() log.info("Clicked ptc button") self.wait_for_page_change(page_title=current_title) - except sel_exceptions: + except sel_exceptions.WebDriverException: log.info( "Could not click button - refreshing and returning to checkout handler" ) @@ -931,7 +931,7 @@ def handle_cart(self): button.click() log.info("Clicked Proceed to Checkout Button") self.wait_for_page_change(page_title=current_page) - except sel_exceptions: + except sel_exceptions.WebDriverException: log.error("Problem clicking Proceed to Checkout button.") log.info("Refreshing page to try again") self.driver.refresh() @@ -1122,7 +1122,7 @@ def get_webdriver_pids(self): self.webdriver_child_pids.append(child.pid) def get_page(self, url): - check_cart_element = None + check_cart_element = [] current_page = [] try: check_cart_element = self.driver.find_element_by_xpath( @@ -1161,7 +1161,7 @@ def show_config(self): ) log.info(f"--Delay of {self.refresh_delay} seconds") if self.headless: - log.info(f"--Browser running in headless mode") + log.info(f"--Headless doesn't work!") if self.used: log.info(f"--Used items are considered for purchase") if self.checkshipping: @@ -1178,6 +1178,8 @@ def show_config(self): log.info(f"--Detailed screenshots/notifications is enabled") if self.log_stock_check: log.info(f"--Additional stock check logging enabled") + if self.testing: + log.warning(f"--Testing Mode. NO Purchases will be made.") if self.slow_mode: log.warning(f"--Slow-mode enabled. Pages will fully load before execution.") if self.shipping_bypass: @@ -1200,6 +1202,10 @@ def show_config(self): log.info(f"--No images will be requested") if not self.notification_handler.sound_enabled: log.info(f"--Notification sounds are disabled.") + if self.headless: + log.warning( + f"--Running headless is unsupported. If you get it to work, please let us know on Discord." + ) if self.testing: log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") From 52bb63775220f11d87bbca084299d6a1cce1d198 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 16:34:32 -0500 Subject: [PATCH 018/150] -- Corrected previous commit to explicitly declared sel_exceptions.WebDriverException in cases where a general error has occured with the web driver. --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index ea7959d7..0dd5a4d3 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1122,7 +1122,7 @@ def get_webdriver_pids(self): self.webdriver_child_pids.append(child.pid) def get_page(self, url): - check_cart_element = [] + check_cart_element = None current_page = [] try: check_cart_element = self.driver.find_element_by_xpath( From 0644cb56221a7011be9b7b23ce159b3a41b0b8d6 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 16:42:13 -0500 Subject: [PATCH 019/150] -- Added missing dependencies --- Pipfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 3179fbb6..f3abd491 100644 --- a/Pipfile +++ b/Pipfile @@ -21,7 +21,7 @@ slackclient = "*" playsound = "*" prompt_toolkit = "*" aiohttp = "*" -pyobjc = {version = "*", sys_platform = "== 'darwin'"} +pyobjc = { version = "*", sys_platform = "== 'darwin'" } async-timeout = "*" amazoncaptcha = "==0.4.4" browser-cookie3 = "*" @@ -29,9 +29,12 @@ coloredlogs = "*" apprise = "*" price-parser = "*" pypresence = "==4.0.0" -pywin32 = {version = "*", sys_platform = "== 'win32'"} +pywin32 = { version = "*", sys_platform = "== 'win32'" } psutil = "*" stdiomask = "*" +packaging = "*" +config = "*" +lxml = "*" [requires] python_version = "3.8" From fc7252e08d1c6b552284180fe9e33a3012f4ce2a Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 9 Jan 2021 10:35:25 -0500 Subject: [PATCH 020/150] Interim checkin for collaboration with other developers who can see this issue on their profiles. Known domains: # AMAZON_DOMAIN = "www.amazon.com.au" # AMAZON_DOMAIN = "www.amazon.com.br" AMAZON_DOMAIN = "www.amazon.ca" # NOT SUPPORTED AMAZON_DOMAIN = "www.amazon.cn" # AMAZON_DOMAIN = "www.amazon.fr" # AMAZON_DOMAIN = "www.amazon.de" # NOT SUPPORTED AMAZON_DOMAIN = "www.amazon.in" # AMAZON_DOMAIN = "www.amazon.it" # AMAZON_DOMAIN = "www.amazon.co.jp" # AMAZON_DOMAIN = "www.amazon.com.mx" # AMAZON_DOMAIN = "www.amazon.nl" # AMAZON_DOMAIN = "www.amazon.es" # AMAZON_DOMAIN = "www.amazon.co.uk" # AMAZON_DOMAIN = "www.amazon.com" # AMAZON_DOMAIN = "www.amazon.se" --- stores/amazon.py | 91 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 0dd5a4d3..09e9f13b 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -45,7 +45,7 @@ # Prime popup # //*[@id="primeAutomaticPopoverAdContent"]/div/div/div[1]/a # //*[@id="primeAutomaticPopoverAdContent"]/div/div/div[1]/a - +FREE_SHIPPING_PRICE = parse_price("0.00") DEFAULT_MAX_CHECKOUT_LOOPS = 20 DEFAULT_MAX_PTC_TRIES = 3 @@ -488,12 +488,18 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): prices = self.driver.find_elements_by_xpath( '//*[@class="a-size-large a-color-price olpOfferPrice a-text-bold"]' ) + if not prices: + # Try the flyout x-paths + prices = self.driver.find_elements_by_xpath( + "//div[@id='aod-offer']//div[@id='aod-offer-price']//span[@class='a-offscreen']" + ) if prices: break if time.time() > timeout: log.info(f"failed to load prices for {asin}, going to next ASIN") return False shipping = [] + shipping_prices = [] if self.checkshipping: timeout = self.get_timeout() while True: @@ -501,7 +507,22 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): '//*[@class="a-color-secondary"]' ) if shipping: + # Convert to prices just in case + for shipping_node in shipping: + if self.checkshipping: + if amazon_config["SHIPPING_ONLY_IF"] in shipping_node.text: + shipping_prices.append(parse_price("0")) + else: + shipping_prices.append(parse_price(shipping_node.text)) + else: + shipping_prices.append(parse_price("0")) break + else: + # Check for offers + offers = self.driver.find_elements_by_xpath.xpath("//div[@id='aod-offer']") + for idx, offer in enumerate(offers): + shipping_prices.append(get_shipping_costs(offer, amazon_config["FREE_SHIPPING"])) + if time.time() > timeout: log.info(f"failed to load shipping for {asin}, going to next ASIN") return False @@ -516,10 +537,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False try: if self.checkshipping: - if amazon_config["SHIPPING_ONLY_IF"] in shipping[idx].text: - ship_price = parse_price("0") - else: - ship_price = parse_price(shipping[idx].text) + ship_price = shipping_prices[idx] else: ship_price = parse_price("0") except IndexError: @@ -1302,3 +1320,66 @@ def get_timestamp_filename(name, extension): return name + "_" + date + extension else: return name + "_" + date + "." + extension + + +def get_shipping_costs(tree, free_shipping_string): + # Assume Free Shipping and change otherwise + + # Shipping collection xpath: + # .//div[starts-with(@id, 'aod-bottlingDepositFee-')]/following-sibling::span + shipping_nodes = tree.xpath( + ".//div[starts-with(@id, 'aod-bottlingDepositFee-')]/following-sibling::*[1]" + ) + count = len(shipping_nodes) + log.debug(f"Found {count} shipping nodes.") + if count == 0: + log.warning("No shipping nodes found. Assuming zero.") + return FREE_SHIPPING_PRICE + elif count > 1: + log.warning("Found multiple shipping nodes. Using the first.") + + shipping_node = shipping_nodes[0] + # Shipping information is found within either a DIV or a SPAN following the bottleDepositFee DIV + # What follows is logic to parse out the various pricing formats within the HTML. Not ideal, but + # it's what we have to work with. + if shipping_node.tag == "div": + if shipping_node.text.strip() == "": + # Assume zero shipping for an empty div + log.debug( + "Empty div found after bottleDepositFee. Assuming zero shipping." + ) + else: + # Assume zero shipping for unknown values in + log.warning( + f"Non-Empty div found after bottleDepositFee. Assuming zero. Stripped Value: '{shipping_node.text.strip()}'" + ) + elif shipping_node.tag == "span": + # Shipping values in the span are contained in either another SPAN or hanging out alone in a B tag + shipping_spans = shipping_node.findall("span") + shipping_bs = shipping_node.findall("b") + shipping_is = shipping_node.findall("i") + if len(shipping_spans) > 0: + # If the span starts with a "& " it's free shipping (right?) + if shipping_spans[0].text.strip() == "&": + # & Free Shipping message + log.debug("Found '& Free', assuming zero.") + elif shipping_spans[0].text.startswith("+"): + return parse_price(shipping_spans[0].text.strip()) + elif len(shipping_bs) > 0: + for message_node in shipping_bs: + + if message_node.text.upper() in free_shipping_string: + log.debug("Found free shipping string.") + else: + log.error( + f"Couldn't parse price from . Assuming 0. Do we need to add: '{message_node.text.upper()}'" + ) + elif len(shipping_is) > 0: + # If it has prime icon class, assume free Prime shipping + if "Free" in shipping_is[0].attrib["aria-label"]: + log.debug("Found Free shipping with Prime") + else: + log.error( + f"Unable to locate price. Assuming 0. Found this: '{shipping_node.text.strip()}'" + ) + return FREE_SHIPPING_PRICE From 979c68bc6d3c39d903765133ff63f15a8ef63939 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 10:11:11 -0500 Subject: [PATCH 021/150] Interim check-in of updates to platforms. Additional headings tagged and content reorganized a bit. --- README.md | 392 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 285 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 16753bac..39c8c12a 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,71 @@ # Fairgame -[Installation](#Installation) | [Usage](#Usage) | [Discord](https://discord.gg/qDY2QBtAW6) | [Troubleshooting](#Troubleshooting) +[Installation](#Installation) | [Usage](#Usage) | [Discord](https://discord.gg/4rfbNKrmnC) +| [Troubleshooting](#Troubleshooting) ## Why??? -We built this in response to the severe tech scalping situation that's happening right now. Almost every tech product that's coming -out right now is being instantly brought out by scalping groups and then resold at at insane prices. $699 GPUs are being listed -for $1700 on eBay, and these scalpers are buying 40 cards while normal consumers can't get a single one. Preorders for the PS5 are -being resold for nearly $1000. Our take on this is that if we release a bot that anyone can use, for free, then the number of items -that scalpers can buy goes down and normal consumers can buy items for MSRP. +We built this in response to the severe tech scalping situation that's happening right now. Almost every tech product +that's coming out right now is being instantly brought out by scalping groups and then resold at at insane prices. $699 +GPUs are being listed for $1700 on eBay, and these scalpers are buying 40 cards while normal consumers can't get a +single one. Preorders for the PS5 are being resold for nearly $1000. Our take on this is that if we release a bot that +anyone can use, for free, then the number of items that scalpers can buy goes down and normal consumers can buy items +for MSRP. **If everyone is botting, then no one is botting.** +## Current Functionality + +| **Website** | **Auto Checkout** | **Open Cart Link** | **Test flag** | +|:---:|:---:|:---:|:---:| +| amazon.com |`✔`| | | +| ~~bestbuy.com~~ | |`✔`| | + +Best Buy has been deprecated, see [details](#best-buy) below. + ## Got a question? -Read through this document and the cheat sheet linked in the next sections. See the [FAQs](#frequently-asked-questions) if that does not answer your questions. +Read through this document and the cheat sheet linked in the next sections. See the [FAQs](#frequently-asked-questions) +if that does not answer your questions. ## Installation -Easy_XII has created a great cheat sheet for getting started, [please follow this guide](https://docs.google.com/document/d/1grN282tPodM9N57bPq4bbNyKZC01t_4A-sLpzzu_7lM/). -**Note:** that we do not control the contents of this document, so use some common sense when configuring the bot. Do not ask us -why the bot does not purchase an $8.49 item when the minimum purchase price is set to $10 in the configuration file that YOU are supposed to update +Community user Easy_XII has created a great cheat sheet for getting started. It includes specific and additional steps +for Windows users as well as useful product and configuration information. Please start +with [this guide](https://docs.google.com/document/d/1grN282tPodM9N57bPq4bbNyKZC01t_4A-sLpzzu_7lM/) to get you started +and to answer any initial questions you may have about setup. + +**Note:** The above document is community maintained and managed. The authors of Fairgame do not control the contents, +so use some common sense when configuring the bot as both the bot and the sites we interact with change over time. For +example, do not ask us why the bot does not purchase an item whose price has changed to $8.49 when the _minimum_ +purchase price is set to $10 in the configuration file that YOU are supposed to update + +### General + +This project uses [Pipenv](https://pypi.org/project/pipenv/) to manage dependencies. Hop in +my [Discord](https://discord.gg/4rfbNKrmnC) if you have ideas, need help or just want to tell us about how you got your +new toys. + +To get started, there are two options: + +#### Releases -This project uses [Pipenv](https://pypi.org/project/pipenv/) to manage dependencies. Hop in my [Discord](https://discord.gg/qDY2QBtAW6) if you have ideas, need help or just want to tell us about how you got your new toys. +To get the latest release as a convenient package, download it directly from +the [Releases](https://github.com/Hari-Nagarajan/fairgame/releases) +page on GitHub. The "Source code" zip or tar file are what you'll want. This can be downloaded and extracted into a +directory of your choice (e.g. C:\fairgame). -To get started you'll first need to clone this repository. If you are unfamiliar with Git, follow the [guide on how to do that on our Wiki](https://github.com/Hari-Nagarajan/fairgame/wiki/How-to-use-GitHub-Desktop-App). You *can* use the "Download Zip" button on the GitHub repository's homepage but this makes receieving updates more difficult. If you can get setup with the GitHub Desktop app, updating to the latest version of the bot takes 1 click. +#### Git + +If you want to manage the code via Git, you'll first need to clone this repository. If you are unfamiliar with Git, +follow the [guide](https://github.com/Hari-Nagarajan/fairgame/wiki/How-to-use-GitHub-Desktop-App) on how to do that on +our Wiki . You *can* use the "Download Zip" button on the GitHub repository's homepage but this makes receiving updates +more difficult. If you can get setup with the GitHub Desktop app, updating to the latest version of the bot takes 1 +click. !!! YOU WILL NEED TO USE THE 3.8 BRANCH OF PYTHON, 3.9.0 BREAKS DEPENDENCIES !!! -``` + +```shell pip install pipenv pipenv shell pipenv install @@ -36,7 +74,8 @@ pipenv install NOTE: YOU SHOULD RUN `pipenv shell` and `pipenv install` ANY TIME YOU UPDATE, IN CASE THE DEPENDENCIES HAVE CHANGED! Run it -``` + +```shell python app.py Usage: app.py [OPTIONS] COMMAND [ARGS]... @@ -48,34 +87,126 @@ Commands: amazon ``` -## Current Functionality +### Platform Specific -| **Website** | **Auto Checkout** | **Open Cart Link** | **Test flag** | -|:---:|:---:|:---:|:---:| -| amazon.com |`✔`| | | -| ~~bestbuy.com~~ | |`✔`| | -Best Buy has been deprecated, see details below. +These instructions are supplied by community members and any adjustments, corrections, improvements or clarifications +are welcome. These are typically created during installation in a single environment, so there may be caveats or changes +necessary for your environment. This isn't intended to be a definitive guide, but a starting point as validation that a +platform can/does work. Please report back any suggestions to our [Discord](https://discord.gg/qDY2QBtAW6) feedback +channel. + +### Installation Ubuntu 20.10 (and probably other distros) + +Based off Ubuntu 20.10 with a fresh installation. + +Open terminal. Either right click desktop and go to Open In Terminal, or search for Terminal under Show Applications + +Install Google Chrome: +`wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && sudo dpkg -i google-chrome-stable_current_amd64.deb` + +Install Pip: +`sudo apt install python3-pip` + +Install pipenv: +`pip3 install pipenv` + +Add /home/$USER/.local/bin to PATH: +`export PATH="/home/$USER/.local/bin:$PATH"` + +Install git: +`sudo apt install git` + +Clone git repository: +`git clone https://github.com/Hari-Nagarajan/fairgame` + +Change into the fairgame folder: +`cd ./fairgame/` + +Prepare your config files within ./config/ + +```shell +cp ./config/amazon_config.template_json ./config/amazon_config.json +cp ./config/apprise.conf_template ./config/apprise.conf +``` + +Make a pipshell environment: +`pipenv shell` + +Install dependencies: +`pipenv install` + +Edit the newly created files with your settings based on your [configuration](#configuration) + +### Installation Raspberry Pi 4 (2 GB+) + +This is an abridged version of the community created document +found [here](https://docs.google.com/document/d/1VUxXhATZ8sZOJxdh3AIY6OGqwLRmrAcPikKZAwphIE8/edit). If the steps here +don't work on your Pi 4, look there for additional options. This hasn't been tested on a Pi 3, but given enough RAM to +run Chrome, it may very well work. Let us know. + +```shell +sudo apt update +sudo apt upgrade +sudo apt install chromium-chromedriver +git clone https://github.com/Hari-Nagarajan/fairgame +cd fairgame/ +pip3 install pipenv +export PATH=$PATH:/home/$USER/.local/bin +pipenv shell +pipenv install +``` + +Leave this Terminal window open. + +Open the following file in a text editor: + +`/home/$USER/.local/share/virtualenvs/fairgame-/lib/python3.7/site-packages/selenium/webdriver/common/service.py` + +Edit line 38 from + +`self.path = executable` + +to + +`self.path = "chromedriver"` + +Then save and close the file. + +Additional steps outside of the readme: + +uncomment requires 3.8 Piplock, as default pi python 2.7.16 works fine + +If you get a compiler error when doing pipenv install run: + +```shell +sudo python3 -m pip install --upgrade pip +sudo apt-get install libffi-dev +sudo apt-get install libzbar-dev +sudo apt-get install clang -y +``` ## Usage -### Amazon -The following flags are specific to the Amazon scripts. They the `[OPTIONS]` to be passed on the command-line to control -the behavior of Amazon scanning and purchasing. These can be added at the command line or added to a batch file/shell - script (see `_Amazon.bat` in the root folder of the project). **NOTE:** `--test` flag has been added to `_Amazon.bat` file by -default. This should be deleted after you've verified that the bot works correctly for you. If you don't want your `_Amazon.bat` +### Amazon + +The following flags are specific to the Amazon scripts. They the `[OPTIONS]` to be passed on the command-line to control +the behavior of Amazon scanning and purchasing. These can be added at the command line or added to a batch file/shell +script (see `_Amazon.bat` in the root folder of the project). **NOTE:** `--test` flag has been added to `_Amazon.bat` +file by default. This should be deleted after you've verified that the bot works correctly for you. If you don't want +your `_Amazon.bat` to be deleted when you update, you should rename it to something else. -**Amazon flags** +#### Amazon flags #### -``` +```shell python app.py amazon --help -Usage: app.py amazon [OPTIONS] +Usage: app.py amazon option Options: --no-image Do not load images --headless Unsupported headless mode. GLHF - --test Run the checkout flow, but do not actually purchase the + --test Run the checkout flow but do not actually purchase the item[s] --delay FLOAT Time to wait between checks for item[s] @@ -109,31 +240,38 @@ Options: --help Show this message and exit. ``` -**Configuration** +#### Configuration -Make a copy of `amazon_config.template_json` and rename to `amazon_config.json`. Edit it according to the ASINs you -are interested in purchasing. [*What's an ASIN?*](https://www.datafeedwatch.com/blog/amazon-asin-number-what-is-it-and-how-do-you-get-it#how-to-find-asin) +Make a copy of `amazon_config.template_json` and rename to `amazon_config.json`. Edit it according to the ASINs you are +interested in purchasing. [*What's an +ASIN?*](https://www.datafeedwatch.com/blog/amazon-asin-number-what-is-it-and-how-do-you-get-it#how-to-find-asin) * `asin_groups` indicates the number of ASIN groups you want to use. -* `asin_list_x` list of ASINs for products you want to purchase. You must locate these (see Discord or lookup the ASIN on product pages). - * The first time an item from list "x" is in stock and under its associated reserve, it will purchase it. +* `asin_list_x` list of ASINs for products you want to purchase. You must locate these (see Discord or lookup the ASIN + on product pages). + * The first time an item from list "x" is in stock and under its associated reserve, it will purchase it. * If the purchase is successful, the bot will not buy anything else from list "x". * Use sequential numbers for x, starting from 1. x can be any integer from 1 to 18,446,744,073,709,551,616 -* `reserve_min_x` set a minimum limit to consider for purchasing an item. If a seller has a listing for a 700 dollar item a 1 dollar, it's likely fake. -* `reserve_max_x` is the most amount you want to spend for a single item (i.e., ASIN) in `asin_list_x`. Does not include tax. If --checkshipping flag is active, this includes shipping listed on offer page. -* `amazon_website` amazon domain you want to use. smile subdomain appears to work better, if available in your country. [*What is Smile?*](https://org.amazon.com/) +* `reserve_min_x` set a minimum limit to consider for purchasing an item. If a seller has a listing for a 700 dollar + item a 1 dollar, it's likely fake. +* `reserve_max_x` is the most amount you want to spend for a single item (i.e., ASIN) in `asin_list_x`. Does not include + tax. If --checkshipping flag is active, this includes shipping listed on offer page. +* `amazon_website` amazon domain you want to use. smile subdomain appears to work better, if available in your + country. [*What is Smile?*](https://org.amazon.com/) -**Examples** +##### Examples One unique product with one ASIN (e.g., Segway Ninebot S and GoKart Drift Kit Bundle) : ```json { - "asin_groups": 1, - "asin_list_1": ["B07K7NLDGT"], - "reserve_min_1": 450, - "reserve_max_1": 500, - "amazon_website": "smile.amazon.com" + "asin_groups": 1, + "asin_list_1": [ + "B07K7NLDGT" + ], + "reserve_min_1": 450, + "reserve_max_1": 500, + "amazon_website": "smile.amazon.com" } ``` @@ -141,44 +279,57 @@ One general product with multiple ASINS (e.g 16 GB USB drive 2 pack) ```json { - "asin_groups": 1, - "asin_list_1": ["B07JH53M4T", "B085M1SQ9S", "B00E9W1ULS"], - "reserve_min_1": 15, - "reserve_max_1": 20, - "amazon_website": "smile.amazon.com" + "asin_groups": 1, + "asin_list_1": [ + "B07JH53M4T", + "B085M1SQ9S", + "B00E9W1ULS" + ], + "reserve_min_1": 15, + "reserve_max_1": 20, + "amazon_website": "smile.amazon.com" } ``` -Two general products with multiple ASINS and different price points (e.g. 16 GB USB drive 2 pack and a statue of The Thinker) +Two general products with multiple ASINS and different price points (e.g. 16 GB USB drive 2 pack and a statue of The +Thinker) ```json { - "asin_groups": 2, - "asin_list_1": ["B07JH53M4T", "B085M1SQ9S", "B00E9W1ULS"], - "reserve_min_1": 15, - "reserve_max_1": 20, - "asin_list_2": ["B006HPI2A2", "B00N54S1WW"], - "reserve_min_2": 50, - "reserve_max_2": 75, - "amazon_website": "smile.amazon.com" + "asin_groups": 2, + "asin_list_1": [ + "B07JH53M4T", + "B085M1SQ9S", + "B00E9W1ULS" + ], + "reserve_min_1": 15, + "reserve_max_1": 20, + "asin_list_2": [ + "B006HPI2A2", + "B00N54S1WW" + ], + "reserve_min_2": 50, + "reserve_max_2": 75, + "amazon_website": "smile.amazon.com" } ``` If you wanted to watch another product, you'd add a third list (e.g. `asin_list_3`) and associated min/max pricing and -increase the `asin_groups` to 3. Add as many lists as are needed, keeping in mind that the main distinction between lists -is the min/max price boundaries. Once any ASIN is purchased from an ASIN list, that list is remove from the hunt +increase the `asin_groups` to 3. Add as many lists as are needed, keeping in mind that the main distinction between +lists is the min/max price boundaries. Once any ASIN is purchased from an ASIN list, that list is remove from the hunt until FairGame is restarted. To verify that your JSON is well formatted, paste and validate it at https://jsonlint.com/ -**Start Up** +#### Start Up -Previously your username and password were entered into the config file, this is no longer the case. On first launch the bot will prompt -you for your credentials. You will then be asked for a password to encrypt them. Once done, your encrypted credentials will be stored in -`amazon_credentials.json`. If you ever forget your encryption password, just delete this file and the next launch of the bot will recreate -it. An example of this will look like the following: +Previously your username and password were entered into the config file, this is no longer the case. On first launch the +bot will prompt you for your credentials. You will then be asked for a password to encrypt them. Once done, your +encrypted credentials will be stored in +`amazon_credentials.json`. If you ever forget your encryption password, just delete this file and the next launch of the +bot will recreate it. An example of this will look like the following: -``` +```shell python app.py amazon INFO Initializing Apprise handler INFO Initializing other notification handlers @@ -194,7 +345,7 @@ INFO Credentials safely stored. Starting the bot when you have created an encrypted file: -``` +```shell python app.py amazon --test INFO Initializing Apprise handler INFO Initializing other notification handlers @@ -202,9 +353,10 @@ INFO Enabled Handlers: ['Audio'] Reading credentials from: amazon_credentials.json Credential file password: ``` + Example usage: -``` +```commandline python app.py amazon --test ... 2020-12-23 13:07:38 INFO Initializing Apprise handler using: config/apprise.conf @@ -237,47 +389,50 @@ python app.py amazon --test ``` - ## ~~Best Buy~~ -Best Buy is currently deprecated because we don't yet have an effective way to determine item availability -without scraping and processing the product pages individually. Future updates may see this functionality -return, but the current code isn't reliable for high demand items and checkout automation has become -increasingly hard due to anti-bot measures taken by Best Buy. +Best Buy is currently deprecated because we don't yet have an effective way to determine item availability without +scraping and processing the product pages individually. Future updates may see this functionality return, but the +current code isn't reliable for high demand items and checkout automation has become increasingly hard due to anti-bot +measures taken by Best Buy. -Original code still exists, but provides very little utility. A 3rd party stock notification service would -probably serve as a better solution at Best Buy. +Original code still exists, but provides very little utility. A 3rd party stock notification service would probably +serve as a better solution at Best Buy. -~~This is fairly basic right now. Just login to the best buy website in your default browser and then run the command as follows:~~ +~~This is fairly basic right now. Just login to the best buy website in your default browser and then run the command as +follows:~~ ``` python app.py bestbuy --sku [SKU] ``` ~~Example:~~ -```python -python app.py bestbuy --sku 6429440 -``` +``` +python python app.py bestbuy - -sku 6429440 +``` ## Notifications ### Sounds -Local sounds are provided as a means to give you audible cues to what is happening. The notification sound -plays for notable events (e.g., start up, product found for purchase) during the scans. An alarm notification -will play when user interaction is necessary. This is typically when all automated options have been exhausted. -Lastly, a purchase notification sound will play if the bot if successful. These local sounds can be disabled -via the command-line and [tested](#testing-notifications) along with other notification methods + +Local sounds are provided as a means to give you audible cues to what is happening. The notification sound plays for +notable events (e.g., start up, product found for purchase) during the scans. An alarm notification will play when user +interaction is necessary. This is typically when all automated options have been exhausted. Lastly, a purchase +notification sound will play if the bot if successful. These local sounds can be disabled via the command-line +and [tested](#testing-notifications) along with other notification methods ### Apprise -Notifications are now handled by Apprise. Apprise lets you send notifications to a large number of supported notification services. -Check https://github.com/caronc/apprise/wiki for a detailed list. -To enable Apprise notifications, make a copy of `apprise.conf_template` in the `config` directory and name it -`apprise.conf`. Then add apprise formatted urls for your desired notification services as simple text entries -in the config file. Any recognized notification services will be reported on app start. +Notifications are now handled by Apprise. Apprise lets you send notifications to a large number of supported +notification services. Check https://github.com/caronc/apprise/wiki for a detailed list. + +To enable Apprise notifications, make a copy of `apprise.conf_template` in the `config` directory and name it +`apprise.conf`. Then add apprise formatted urls for your desired notification services as simple text entries in the +config file. Any recognized notification services will be reported on app start. + +##### Apprise Example Config: -**Apprise Example Config:** ``` # Hash Tags denote comment lines and blank lines are allowed # Discord (https://github.com/caronc/apprise/wiki/Notify_discord) @@ -293,44 +448,57 @@ https://hooks.slack.com/services/{tokenA}/{tokenB}/{tokenC} ``` -#### Pavlok -To enable shock notifications to your [Pavlok Shockwatch](https://www.amazon.com/Pavlok-PAV2-PERIMETER-BLACK-2/dp/B01N8VJX8P?), -store the url from the pavlok app in the ```pavlok_config.json``` file, you can copy the template from ```pavlok_config.template_json```. +### Pavlok + +To enable shock notifications to +your [Pavlok Shockwatch](https://www.amazon.com/Pavlok-PAV2-PERIMETER-BLACK-2/dp/B01N8VJX8P?), store the url from the +pavlok app in the ```pavlok_config.json``` file, you can copy the template from ```pavlok_config.template_json```. **WARNING:** This feature does not currently support adjusting the intensity, it will always be max (255). + ```json { "base_url": "url goes here" } ``` +### Testing notifications -#### Testing notifications - -Once you have setup your `apprise_config.json ` you can test it by running `python app.py test-notifications` from within your pipenv shell. This will send a test notification to all configured notification services. +Once you have setup your `apprise_config.json ` you can test it by running `python app.py test-notifications` from +within your pipenv shell. This will send a test notification to all configured notification services. ## Troubleshooting -Re-read this documentation. Verify your JSON. +Re-read this documentation. Verify your JSON. + +Consider joining the #tech-support channel in [Discord](https://discord.gg/5tw6UY7g44) for help from the community if +these common fixes don't help. -I suggest joining the #tech-support channel in [Discord](https://discord.gg/qDY2QBtAW6) for help from the community if these common fixes don't help. +**Error: ```selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary```** +The issue is that chrome is not installed in the expected location. +See [Selenium Wiki](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver#requirements) and the section +on [overriding the Chrome binary location .](https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-Using-a-Chrome-executable-in-a-non-standard-location) -**Error: ```selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary```** -The issue is that chrome is not installed in the expected location. See [Selenium Wiki](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver#requirements) and the section on [overriding the Chrome binary location .](https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-Using-a-Chrome-executable-in-a-non-standard-location) +The easy fix for this is to add an option where selenium is used (`selenium_utils.py`) -The easy fix for this is to add an option where selenium is used (`selenium_utils.py``) -```python -chrome_options.binary_location="C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe" +``` +python chrome_options.binary_location = "C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe" ``` -**Error: ```selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 87```** +** +Error: ```selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 87```** -You are not running the proper version of Chrome this requires. As of this update, the current version is Chrome 87. Check your version by going to ```chrome://version/``` in your browser. We are going to be targeting the current stable build of chrome. If you are behind, please update, if you are on a beta or canary branch, you'll have to build your own version of chromedriver-py. +You are not running the proper version of Chrome this requires. As of this update, the current version is Chrome 87. +Check your version by going to ```chrome://version/``` in your browser. We are going to be targeting the current stable +build of chrome. If you are behind, please update, if you are on a beta or canary branch, you'll have to build your own +version of chromedriver-py. ## Raspberry-Pi-Setup + Maybe this works? 1. Prereqs and Setup + ```shell sudo apt update sudo apt upgrade @@ -342,16 +510,20 @@ export PATH=$PATH:/home//.local/bin pipenv shell pipenv install ``` + 2. Leave this Terminal window open. -3. Open the following file in a text editor: +3. Open the following file in a text editor: + ``` /home//.local/share/virtualenvs/fairgame-/lib/python3.7/site-packages/selenium/webdriver/common/service.py ``` + 4. Edit line 38 from `self.path = executable` to `self.path = "chromedriver"`, then save and close the file. 5. Back in Terminal... + ```shell python app.py ``` @@ -360,10 +532,16 @@ python app.py ## Frequently Asked Questions -### 1. Can I run multiple instances of the bot? -Yes. For example you can run one instance to check stock on Best Buy and a separate instance to check stock on Amazon. Bear in mind that if you do this you may end up with multiple purchases going through at the same time. +To keep up with questions, the Discord channel [#FAQ](https://discord.gg/GEsarYKMAw) is where you'll find the latest +answers. If you don't find it there, ask in #tech-support. + +### 1. Can I run multiple instances of the bot? + +Yes. For example you can run one instance to check stock on Best Buy and a separate instance to check stock on Amazon. +Bear in mind that if you do this you may end up with multiple purchases going through at the same time. ### 2. Does Fairgame automatically bypass CAPTCHA's on the store sites? + * For Amazon, yes. The bot will try and auto-solve CAPTCHA's during the checkout process. ## Attribution From 383341093885b7a17f2110fd9d5bc0c89a785f78 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 10:33:39 -0500 Subject: [PATCH 022/150] --Updated headless flag --Added headless FAQ --Added Raspberry Pi FAQ --- README.md | 91 +++++++++++++++++------------------------------- stores/amazon.py | 8 +---- 2 files changed, 33 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 39c8c12a..ec2d11ae 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ Usage: app.py amazon option Options: --no-image Do not load images - --headless Unsupported headless mode. GLHF + --headless Runs Chrome in headless mode. --test Run the checkout flow but do not actually purchase the item[s] @@ -469,80 +469,53 @@ within your pipenv shell. This will send a test notification to all configured n ## Troubleshooting -Re-read this documentation. Verify your JSON. ++ Re-read this documentation. -Consider joining the #tech-support channel in [Discord](https://discord.gg/5tw6UY7g44) for help from the community if -these common fixes don't help. ++ Verify your JSON. -**Error: ```selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary```** -The issue is that chrome is not installed in the expected location. -See [Selenium Wiki](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver#requirements) and the section -on [overriding the Chrome binary location .](https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-Using-a-Chrome-executable-in-a-non-standard-location) ++ Consider joining the #tech-support channel in [Discord](https://discord.gg/5tw6UY7g44) for help from the community if + these common fixes don't help. -The easy fix for this is to add an option where selenium is used (`selenium_utils.py`) ++ **Error: ```selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary```** + The issue is that chrome is not installed in the expected location. + See [Selenium Wiki](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver#requirements) and the section + on [overriding the Chrome binary location .](https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-Using-a-Chrome-executable-in-a-non-standard-location) -``` -python chrome_options.binary_location = "C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe" -``` - -** -Error: ```selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 87```** - -You are not running the proper version of Chrome this requires. As of this update, the current version is Chrome 87. -Check your version by going to ```chrome://version/``` in your browser. We are going to be targeting the current stable -build of chrome. If you are behind, please update, if you are on a beta or canary branch, you'll have to build your own -version of chromedriver-py. + The easy fix for this is to add an option where selenium is used (`selenium_utils.py`) -## Raspberry-Pi-Setup + ``` + python chrome_options.binary_location = "C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe" + ``` -Maybe this works? ++ **Error: ```selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 87```** -1. Prereqs and Setup - -```shell -sudo apt update -sudo apt upgrade -sudo apt install chromium-chromedriver -git clone https://github.com/Hari-Nagarajan/fairgame -cd fairgame/ -pip3 install pipenv -export PATH=$PATH:/home//.local/bin -pipenv shell -pipenv install -``` - -2. Leave this Terminal window open. - -3. Open the following file in a text editor: - -``` -/home//.local/share/virtualenvs/fairgame-/lib/python3.7/site-packages/selenium/webdriver/common/service.py -``` - -4. Edit line 38 from `self.path = executable` to `self.path = "chromedriver"`, then save and close the file. - - -5. Back in Terminal... - -```shell -python app.py -``` - -6. Follow [Usage](#Usage) to configure the bot as needed. + You are not running the proper version of Chrome this requires. As of this update, the current version is Chrome 87. + Check your version by going to ```chrome://version/``` in your browser. We are going to be targeting the current stable + build of chrome. If you are behind, please update, if you are on a beta or canary branch, you'll have to build your own + version of chromedriver-py. ## Frequently Asked Questions To keep up with questions, the Discord channel [#FAQ](https://discord.gg/GEsarYKMAw) is where you'll find the latest answers. If you don't find it there, ask in #tech-support. -### 1. Can I run multiple instances of the bot? +1. **Can I run multiple instances of the bot?** + + Yes. For example you can run one instance to check stock on Best Buy and a separate instance to check stock on + Amazon. Bear in mind that if you do this you may end up with multiple purchases going through at the same time. -Yes. For example you can run one instance to check stock on Best Buy and a separate instance to check stock on Amazon. -Bear in mind that if you do this you may end up with multiple purchases going through at the same time. +2. **Does Fairgame automatically bypass CAPTCHA's on the store sites?** + For Amazon, yes. The bot will try and auto-solve CAPTCHA's during the checkout process. -### 2. Does Fairgame automatically bypass CAPTCHA's on the store sites? +3. **Does `--headless` work?** + Yes! A community user identified the issue with the headless option while running on a Raspberry Pi. This allowed + the developers to update the codebase to consistently work correctly on headless server environments. Give it a try + and let us know if you have any issues. -* For Amazon, yes. The bot will try and auto-solve CAPTCHA's during the checkout process. +4. **Does Fairgame run on a Raspberry Pi?** + Yes, with caveats. Most people seem to have success with Raspberry Pi 4. The 2 GB model may need to run the headless + option due to the smaller memory footprint. Still awaiting community feedback on running on a Pi 3. CPU and memory + capacity seem to be the limiting factor for older Pi models. ## Attribution diff --git a/stores/amazon.py b/stores/amazon.py index 09e9f13b..882b8f7f 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1179,7 +1179,7 @@ def show_config(self): ) log.info(f"--Delay of {self.refresh_delay} seconds") if self.headless: - log.info(f"--Headless doesn't work!") + log.info(f"--Browser running in headless mode") if self.used: log.info(f"--Used items are considered for purchase") if self.checkshipping: @@ -1196,8 +1196,6 @@ def show_config(self): log.info(f"--Detailed screenshots/notifications is enabled") if self.log_stock_check: log.info(f"--Additional stock check logging enabled") - if self.testing: - log.warning(f"--Testing Mode. NO Purchases will be made.") if self.slow_mode: log.warning(f"--Slow-mode enabled. Pages will fully load before execution.") if self.shipping_bypass: @@ -1220,10 +1218,6 @@ def show_config(self): log.info(f"--No images will be requested") if not self.notification_handler.sound_enabled: log.info(f"--Notification sounds are disabled.") - if self.headless: - log.warning( - f"--Running headless is unsupported. If you get it to work, please let us know on Discord." - ) if self.testing: log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") From ec9ea58ab64d9eeef10e2925a7a215885234b0c5 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 14:27:57 -0500 Subject: [PATCH 023/150] -- Removed reference to older version of Python on Pi --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index ec2d11ae..d8884862 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Edit the newly created files with your settings based on your [configuration](#c ### Installation Raspberry Pi 4 (2 GB+) -This is an abridged version of the community created document +This is an abridged version of the community created document by UnidentifiedWarlock and Judarius. It can be found [here](https://docs.google.com/document/d/1VUxXhATZ8sZOJxdh3AIY6OGqwLRmrAcPikKZAwphIE8/edit). If the steps here don't work on your Pi 4, look there for additional options. This hasn't been tested on a Pi 3, but given enough RAM to run Chrome, it may very well work. Let us know. @@ -174,8 +174,6 @@ Then save and close the file. Additional steps outside of the readme: -uncomment requires 3.8 Piplock, as default pi python 2.7.16 works fine - If you get a compiler error when doing pipenv install run: ```shell From 4f4a9469f9f743d298cfcd375226060687fd7236 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+dakkjaniels@users.noreply.github.com> Date: Tue, 5 Jan 2021 14:47:48 -0500 Subject: [PATCH 024/150] -- Explicitly declared sel_exceptions.WebDriverException in cases where a general error has occured with the web driver. --- stores/amazon.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 882b8f7f..f038e4af 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1140,7 +1140,7 @@ def get_webdriver_pids(self): self.webdriver_child_pids.append(child.pid) def get_page(self, url): - check_cart_element = None + check_cart_element = [] current_page = [] try: check_cart_element = self.driver.find_element_by_xpath( @@ -1179,7 +1179,7 @@ def show_config(self): ) log.info(f"--Delay of {self.refresh_delay} seconds") if self.headless: - log.info(f"--Browser running in headless mode") + log.info(f"--Headless doesn't work!") if self.used: log.info(f"--Used items are considered for purchase") if self.checkshipping: @@ -1196,6 +1196,8 @@ def show_config(self): log.info(f"--Detailed screenshots/notifications is enabled") if self.log_stock_check: log.info(f"--Additional stock check logging enabled") + if self.testing: + log.warning(f"--Testing Mode. NO Purchases will be made.") if self.slow_mode: log.warning(f"--Slow-mode enabled. Pages will fully load before execution.") if self.shipping_bypass: @@ -1218,6 +1220,10 @@ def show_config(self): log.info(f"--No images will be requested") if not self.notification_handler.sound_enabled: log.info(f"--Notification sounds are disabled.") + if self.headless: + log.warning( + f"--Running headless is unsupported. If you get it to work, please let us know on Discord." + ) if self.testing: log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") From 7bec98ba0ff36f92b6562d7b74c94b9f73d42611 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 5 Jan 2021 16:34:32 -0500 Subject: [PATCH 025/150] -- Corrected previous commit to explicitly declared sel_exceptions.WebDriverException in cases where a general error has occured with the web driver. --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index f038e4af..09e9f13b 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1140,7 +1140,7 @@ def get_webdriver_pids(self): self.webdriver_child_pids.append(child.pid) def get_page(self, url): - check_cart_element = [] + check_cart_element = None current_page = [] try: check_cart_element = self.driver.find_element_by_xpath( From 3a0c000e4c601d180e90fceae956a310d1d8e823 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 11 Jan 2021 11:46:21 -0500 Subject: [PATCH 026/150] -- Added FREE SHIPPING string collection to configuration --- config/fairgame.conf | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/fairgame.conf b/config/fairgame.conf index ad27ae1b..0044d399 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -105,6 +105,16 @@ "There are currently no listings for this search. Try a different refinement.", "There are currently no listings for this search. Try a different refinement.", "There are currently no listings for this product in . Try changing the condition type." + ], + "FREE_SHIPPING":[ + "FREE SHIPPING", + "GRATIS BEZORGING", + "FRETE GRÁTIS", + "LIVRAISON GRATUITE", + "FREE DELIVERY.", + "FREE DELIVERY", + "ENVÍO GRATIS.", + "FRI FRAKT" ] } } \ No newline at end of file From a45308729b7e36e4906f063bf154c775ffb783c9 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 11 Jan 2021 12:43:52 -0500 Subject: [PATCH 027/150] Interim commit with checks for both offer listing types (offer page and flyout). Logging found offers per page as an intermediate step to indicate what the xpath code finds in the source. Also using Selenium driver waits to determine that we have an offer node to parse. --- stores/amazon.py | 60 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 09e9f13b..aa18a63b 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -10,6 +10,7 @@ from amazoncaptcha import AmazonCaptcha from chromedriver_py import binary_path # this will get you the path variable from furl import furl +from lxml import html from price_parser import parse_price from pypresence import exceptions as pyexceptions from selenium import webdriver @@ -462,9 +463,43 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): timeout = self.get_timeout() while True: + # Sanity check to see if we have any offers + try: + offers_exist = WebDriverWait(self.driver, timeout=5).until(lambda d: d.find_element_by_xpath( + "//div[@id='aod-offer-list'] | //div[@id='olpOfferList']" + )) + offer_count = [] + if offers_exist.get_attribute("id") == "olpOfferList": + # Offers Page ... count the 'a-row' classes to know how many offers we 'see' + offer_count = self.driver.find_elements_by_xpath("//div[contains(@class, 'a-row')]") + elif offers_exist.get_attribute("id") == "aod-offer-list": + # Offer Flyout or Ajax call ... count the 'aod-offer' divs that we 'see' + offer_count = self.driver.find_elements_by_xpath("//div[@id='aod-offer']") + log.info(f"Found {len(offer_count)} offers in the HTML.") + + except sel_exceptions.TimeoutException: + log.error("Timed out waiting for offers to render. Skipping...") + return False + except sel_exceptions.NoSuchElementException: + log.error("Unable to find any offers listing. Skipping...") + return False + + atc_buttons = self.driver.find_elements_by_xpath( '//*[@name="submit.addToCart"]' ) + if not atc_buttons: + # Sanity check to see if we have a valid page, but no offers: + offer_count = WebDriverWait(self.driver, timeout=5).until(lambda d: d.find_element_by_xpath( + "//div[@id='aod-offer-list']//input[@id='aod-total-offer-count']" + )) + + # offer_count = self.driver.find_element_by_xpath( + # "//div[@id='aod-offer-list']//input[@id='aod-total-offer-count']" + # ) + if offer_count.get_attribute("value") == "0": + log.info("Found zero offers explicitly. Moving to next ASIN.") + return False if atc_buttons: # Early out if we found buttons break @@ -484,6 +519,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False timeout = self.get_timeout() + flyout_mode = False while True: prices = self.driver.find_elements_by_xpath( '//*[@class="a-size-large a-color-price olpOfferPrice a-text-bold"]' @@ -491,8 +527,11 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): if not prices: # Try the flyout x-paths prices = self.driver.find_elements_by_xpath( - "//div[@id='aod-offer']//div[@id='aod-offer-price']//span[@class='a-offscreen']" + "//div[@id='aod-offer']//div[@id='aod-offer-price']//span[@class='a-price']//span[@class='a-offscreen']" ) + if prices: + flyout_mode = True + break if prices: break if time.time() > timeout: @@ -503,9 +542,10 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): if self.checkshipping: timeout = self.get_timeout() while True: - shipping = self.driver.find_elements_by_xpath( - '//*[@class="a-color-secondary"]' - ) + if not flyout_mode: + shipping = self.driver.find_elements_by_xpath( + '//*[@class="a-color-secondary"]' + ) if shipping: # Convert to prices just in case for shipping_node in shipping: @@ -516,12 +556,14 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): shipping_prices.append(parse_price(shipping_node.text)) else: shipping_prices.append(parse_price("0")) - break else: # Check for offers - offers = self.driver.find_elements_by_xpath.xpath("//div[@id='aod-offer']") + offers = self.driver.find_elements_by_xpath("//div[@id='aod-offer']") for idx, offer in enumerate(offers): - shipping_prices.append(get_shipping_costs(offer, amazon_config["FREE_SHIPPING"])) + tree = html.fromstring(offer.get_attribute('innerHTML')) + shipping_prices.append(get_shipping_costs(tree, amazon_config["FREE_SHIPPING"])) + if shipping_prices: + break if time.time() > timeout: log.info(f"failed to load shipping for {asin}, going to next ASIN") @@ -1179,7 +1221,7 @@ def show_config(self): ) log.info(f"--Delay of {self.refresh_delay} seconds") if self.headless: - log.info(f"--Headless doesn't work!") + log.info(f"--Chrome is running in Headless mode") if self.used: log.info(f"--Used items are considered for purchase") if self.checkshipping: @@ -1196,8 +1238,6 @@ def show_config(self): log.info(f"--Detailed screenshots/notifications is enabled") if self.log_stock_check: log.info(f"--Additional stock check logging enabled") - if self.testing: - log.warning(f"--Testing Mode. NO Purchases will be made.") if self.slow_mode: log.warning(f"--Slow-mode enabled. Pages will fully load before execution.") if self.shipping_bypass: From 7cbf08e1692f1c9971678c6f496ae8bc6bf4556b Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 12 Jan 2021 11:49:36 -0500 Subject: [PATCH 028/150] Interim checkin with verbose logging on flyout and PDP page interactions. --- config/fairgame.conf | 3 +- stores/amazon.py | 65 ++++++++++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 0044d399..9b34ccd8 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -114,7 +114,8 @@ "FREE DELIVERY.", "FREE DELIVERY", "ENVÍO GRATIS.", - "FRI FRAKT" + "FRI FRAKT", + "GRATIS-LIEFERUNG" ] } } \ No newline at end of file diff --git a/stores/amazon.py b/stores/amazon.py index aa18a63b..37b02e43 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -25,7 +25,7 @@ AMAZON_URLS = { "BASE_URL": "https://{domain}/", - "OFFER_URL": "https://{domain}/gp/offer-listing/", + "OFFER_URL": "https://{domain}//gp/product/", "CART_URL": "https://{domain}/gp/cart/view.html", } CHECKOUT_URL = "https://{domain}/gp/cart/desktop/go-to-checkout.html/ref=ox_sc_proceed?partialCheckoutCart=1&isToBeGiftWrappedBefore=0&proceedToRetailCheckout=Proceed+to+checkout&proceedToCheckout=1&cartInitiateId={cart_id}" @@ -65,20 +65,20 @@ class Amazon: def __init__( - self, - notification_handler, - headless=False, - checkshipping=False, - detailed=False, - used=False, - single_shot=False, - no_screenshots=False, - disable_presence=False, - slow_mode=False, - no_image=False, - encryption_pass=None, - log_stock_check=False, - shipping_bypass=False, + self, + notification_handler, + headless=False, + checkshipping=False, + detailed=False, + used=False, + single_shot=False, + no_screenshots=False, + disable_presence=False, + slow_mode=False, + no_image=False, + encryption_pass=None, + log_stock_check=False, + shipping_bypass=False, ): self.notification_handler = notification_handler self.asin_list = [] @@ -220,9 +220,9 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): pass # if successful after running navigate pages, remove the asin_list from the list if ( - not self.try_to_checkout - and not self.single_shot - and self.great_success + not self.try_to_checkout + and not self.single_shot + and self.great_success ): self.remove_asin_list(asin) # checkout loop limiters @@ -466,7 +466,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): # Sanity check to see if we have any offers try: offers_exist = WebDriverWait(self.driver, timeout=5).until(lambda d: d.find_element_by_xpath( - "//div[@id='aod-offer-list'] | //div[@id='olpOfferList']" + "//div[@id='aod-offer-list'] | //div[@id='olpOfferList'] | //span[@data-action='show-all-offers-display']" )) offer_count = [] if offers_exist.get_attribute("id") == "olpOfferList": @@ -475,7 +475,17 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): elif offers_exist.get_attribute("id") == "aod-offer-list": # Offer Flyout or Ajax call ... count the 'aod-offer' divs that we 'see' offer_count = self.driver.find_elements_by_xpath("//div[@id='aod-offer']") - log.info(f"Found {len(offer_count)} offers in the HTML.") + else: + # No offers to parse... look for a link to the offers + log.info("Attempting to click the open offers link...") + self.driver.find_element_by_xpath( + "//span[@data-action='show-all-offers-display']//a").click() + # Now wait for the flyout to load + log.info("Waiting for flyout... probably") + WebDriverWait(self.driver, timeout=5).until(lambda d: d.find_element_by_xpath("//div[@id='aod-container']")) + log.info("It flew out?!") + continue + log.info(f"Found {len(offer_count)} offers in the HTML. Attempting to parse...") except sel_exceptions.TimeoutException: log.error("Timed out waiting for offers to render. Skipping...") @@ -484,7 +494,6 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): log.error("Unable to find any offers listing. Skipping...") return False - atc_buttons = self.driver.find_elements_by_xpath( '//*[@name="submit.addToCart"]' ) @@ -570,6 +579,8 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False in_stock = False + for shipping_price in shipping_prices: + log.info(f"\tShipping Price: {shipping_price}") for idx, atc_button in enumerate(atc_buttons): try: @@ -593,11 +604,11 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ship_float = 0 if ( - (ship_float + price_float) <= reserve_max - or math.isclose((price_float + ship_float), reserve_max, abs_tol=0.01) + (ship_float + price_float) <= reserve_max + or math.isclose((price_float + ship_float), reserve_max, abs_tol=0.01) ) and ( - (ship_float + price_float) >= reserve_min - or math.isclose((price_float + ship_float), reserve_min, abs_tol=0.01) + (ship_float + price_float) >= reserve_min + or math.isclose((price_float + ship_float), reserve_min, abs_tol=0.01) ): log.info("Item in stock and in reserve range!") log.info("clicking add to cart") @@ -1069,7 +1080,7 @@ def handle_captcha(self): current_page = self.driver.title try: if self.driver.find_element_by_xpath( - '//form[@action="/errors/validateCaptcha"]' + '//form[@action="/errors/validateCaptcha"]' ): try: log.info("Stuck on a captcha... Lets try to solve it.") @@ -1153,7 +1164,7 @@ def save_page_source(self, page): def wait_for_page_change(self, page_title, timeout=3): time_to_end = self.get_timeout(timeout=timeout) while time.time() < time_to_end and ( - self.driver.title == page_title or not self.driver.title + self.driver.title == page_title or not self.driver.title ): pass if self.driver.title != page_title: From 111722a4af62a7845da19ce313f1c5e26f3a16b4 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 12 Jan 2021 12:00:04 -0500 Subject: [PATCH 029/150] Corrected offer_url from testing to correct offers url --- stores/amazon.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 37b02e43..413eda6e 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -25,7 +25,7 @@ AMAZON_URLS = { "BASE_URL": "https://{domain}/", - "OFFER_URL": "https://{domain}//gp/product/", + "OFFER_URL": "https://{domain}/gp/offer-listing/", "CART_URL": "https://{domain}/gp/cart/view.html", } CHECKOUT_URL = "https://{domain}/gp/cart/desktop/go-to-checkout.html/ref=ox_sc_proceed?partialCheckoutCart=1&isToBeGiftWrappedBefore=0&proceedToRetailCheckout=Proceed+to+checkout&proceedToCheckout=1&cartInitiateId={cart_id}" @@ -482,7 +482,8 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): "//span[@data-action='show-all-offers-display']//a").click() # Now wait for the flyout to load log.info("Waiting for flyout... probably") - WebDriverWait(self.driver, timeout=5).until(lambda d: d.find_element_by_xpath("//div[@id='aod-container']")) + WebDriverWait(self.driver, timeout=5).until( + lambda d: d.find_element_by_xpath("//div[@id='aod-container']")) log.info("It flew out?!") continue log.info(f"Found {len(offer_count)} offers in the HTML. Attempting to parse...") From 37ad9818957beec2756fd3fa5fd03db7fe05d52a Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 12 Jan 2021 19:04:26 -0500 Subject: [PATCH 030/150] Interim checkin with verbose logging for flyout page --- config/fairgame.conf | 3 +- stores/amazon.py | 133 ++++++++++++++++++++++++++++--------------- 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 9b34ccd8..99842f4c 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -115,7 +115,8 @@ "FREE DELIVERY", "ENVÍO GRATIS.", "FRI FRAKT", - "GRATIS-LIEFERUNG" + "GRATIS-LIEFERUNG", + "PRIME FREE DELIVERY" ] } } \ No newline at end of file diff --git a/stores/amazon.py b/stores/amazon.py index 413eda6e..e94b099a 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -23,6 +23,7 @@ from utils.logger import log from utils.selenium_utils import options, enable_headless +# Optional OFFER_URL is: "OFFER_URL": "https://{domain}/dp/", AMAZON_URLS = { "BASE_URL": "https://{domain}/", "OFFER_URL": "https://{domain}/gp/offer-listing/", @@ -65,20 +66,20 @@ class Amazon: def __init__( - self, - notification_handler, - headless=False, - checkshipping=False, - detailed=False, - used=False, - single_shot=False, - no_screenshots=False, - disable_presence=False, - slow_mode=False, - no_image=False, - encryption_pass=None, - log_stock_check=False, - shipping_bypass=False, + self, + notification_handler, + headless=False, + checkshipping=False, + detailed=False, + used=False, + single_shot=False, + no_screenshots=False, + disable_presence=False, + slow_mode=False, + no_image=False, + encryption_pass=None, + log_stock_check=False, + shipping_bypass=False, ): self.notification_handler = notification_handler self.asin_list = [] @@ -220,9 +221,9 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): pass # if successful after running navigate pages, remove the asin_list from the list if ( - not self.try_to_checkout - and not self.single_shot - and self.great_success + not self.try_to_checkout + and not self.single_shot + and self.great_success ): self.remove_asin_list(asin) # checkout loop limiters @@ -465,28 +466,50 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): while True: # Sanity check to see if we have any offers try: - offers_exist = WebDriverWait(self.driver, timeout=5).until(lambda d: d.find_element_by_xpath( - "//div[@id='aod-offer-list'] | //div[@id='olpOfferList'] | //span[@data-action='show-all-offers-display']" - )) + offers_exist = WebDriverWait(self.driver, timeout=5).until( + lambda d: d.find_element_by_xpath( + "//div[@id='aod-offer-list'] | " + "//div[@id='olpOfferList'] | " + "//span[@data-action='show-all-offers-display'] |" + "//input[@name='submit.add-to-cart' and not(//span[@data-action='show-all-offers-display'])]" + ) + ) offer_count = [] if offers_exist.get_attribute("id") == "olpOfferList": # Offers Page ... count the 'a-row' classes to know how many offers we 'see' - offer_count = self.driver.find_elements_by_xpath("//div[contains(@class, 'a-row')]") + offer_count = self.driver.find_elements_by_xpath( + "//div[contains(@class, 'a-row')]" + ) elif offers_exist.get_attribute("id") == "aod-offer-list": # Offer Flyout or Ajax call ... count the 'aod-offer' divs that we 'see' - offer_count = self.driver.find_elements_by_xpath("//div[@id='aod-offer']") - else: + offer_count = self.driver.find_elements_by_xpath( + "//div[@id='aod-offer']" + ) + elif ( + offers_exist.get_attribute("data-action") + == "show-all-offers-display" + ): # No offers to parse... look for a link to the offers log.info("Attempting to click the open offers link...") self.driver.find_element_by_xpath( - "//span[@data-action='show-all-offers-display']//a").click() + "//span[@data-action='show-all-offers-display']//a" + ).click() # Now wait for the flyout to load log.info("Waiting for flyout... probably") WebDriverWait(self.driver, timeout=5).until( - lambda d: d.find_element_by_xpath("//div[@id='aod-container']")) + lambda d: d.find_element_by_xpath("//div[@id='aod-container']") + ) log.info("It flew out?!") continue - log.info(f"Found {len(offer_count)} offers in the HTML. Attempting to parse...") + else: + # This assumes we're on a PDP with only an add to cart button... no offers + log.warning( + "NOT YET IMPLEMENTED: PDP represents only item worth considering. Parse pricing and Add To Cart from PDP if item qualifies." + ) + return False + log.info( + f"Found {len(offer_count)} offers in the HTML. Attempting to parse..." + ) except sel_exceptions.TimeoutException: log.error("Timed out waiting for offers to render. Skipping...") @@ -500,9 +523,11 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) if not atc_buttons: # Sanity check to see if we have a valid page, but no offers: - offer_count = WebDriverWait(self.driver, timeout=5).until(lambda d: d.find_element_by_xpath( - "//div[@id='aod-offer-list']//input[@id='aod-total-offer-count']" - )) + offer_count = WebDriverWait(self.driver, timeout=5).until( + lambda d: d.find_element_by_xpath( + "//div[@id='aod-offer-list']//input[@id='aod-total-offer-count']" + ) + ) # offer_count = self.driver.find_element_by_xpath( # "//div[@id='aod-offer-list']//input[@id='aod-total-offer-count']" @@ -558,7 +583,8 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) if shipping: # Convert to prices just in case - for shipping_node in shipping: + for idx, shipping_node in enumerate(shipping): + log.debug(f"Processing shipping node {idx}") if self.checkshipping: if amazon_config["SHIPPING_ONLY_IF"] in shipping_node.text: shipping_prices.append(parse_price("0")) @@ -568,10 +594,14 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): shipping_prices.append(parse_price("0")) else: # Check for offers - offers = self.driver.find_elements_by_xpath("//div[@id='aod-offer']") + offers = self.driver.find_elements_by_xpath( + "//div[@id='aod-offer']" + ) for idx, offer in enumerate(offers): - tree = html.fromstring(offer.get_attribute('innerHTML')) - shipping_prices.append(get_shipping_costs(tree, amazon_config["FREE_SHIPPING"])) + tree = html.fromstring(offer.get_attribute("innerHTML")) + shipping_prices.append( + get_shipping_costs(tree, amazon_config["FREE_SHIPPING"]) + ) if shipping_prices: break @@ -581,7 +611,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): in_stock = False for shipping_price in shipping_prices: - log.info(f"\tShipping Price: {shipping_price}") + log.debug(f"\tShipping Price: {shipping_price}") for idx, atc_button in enumerate(atc_buttons): try: @@ -605,11 +635,11 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ship_float = 0 if ( - (ship_float + price_float) <= reserve_max - or math.isclose((price_float + ship_float), reserve_max, abs_tol=0.01) + (ship_float + price_float) <= reserve_max + or math.isclose((price_float + ship_float), reserve_max, abs_tol=0.01) ) and ( - (ship_float + price_float) >= reserve_min - or math.isclose((price_float + ship_float), reserve_min, abs_tol=0.01) + (ship_float + price_float) >= reserve_min + or math.isclose((price_float + ship_float), reserve_min, abs_tol=0.01) ): log.info("Item in stock and in reserve range!") log.info("clicking add to cart") @@ -1081,7 +1111,7 @@ def handle_captcha(self): current_page = self.driver.title try: if self.driver.find_element_by_xpath( - '//form[@action="/errors/validateCaptcha"]' + '//form[@action="/errors/validateCaptcha"]' ): try: log.info("Stuck on a captcha... Lets try to solve it.") @@ -1165,7 +1195,7 @@ def save_page_source(self, page): def wait_for_page_change(self, page_title, timeout=3): time_to_end = self.get_timeout(timeout=timeout) while time.time() < time_to_end and ( - self.driver.title == page_title or not self.driver.title + self.driver.title == page_title or not self.driver.title ): pass if self.driver.title != page_title: @@ -1394,8 +1424,9 @@ def get_shipping_costs(tree, free_shipping_string): # Shipping information is found within either a DIV or a SPAN following the bottleDepositFee DIV # What follows is logic to parse out the various pricing formats within the HTML. Not ideal, but # it's what we have to work with. + shipping_span_text = shipping_node.text.strip() if shipping_node.tag == "div": - if shipping_node.text.strip() == "": + if shipping_span_text == "": # Assume zero shipping for an empty div log.debug( "Empty div found after bottleDepositFee. Assuming zero shipping." @@ -1403,13 +1434,20 @@ def get_shipping_costs(tree, free_shipping_string): else: # Assume zero shipping for unknown values in log.warning( - f"Non-Empty div found after bottleDepositFee. Assuming zero. Stripped Value: '{shipping_node.text.strip()}'" + f"Non-Empty div found after bottleDepositFee. Assuming zero. Stripped Value: '{shipping_span_text}'" ) elif shipping_node.tag == "span": - # Shipping values in the span are contained in either another SPAN or hanging out alone in a B tag + # Shipping values in the span are contained in: + # - another SPAN + # - hanging out alone in a B tag + # - Hanging out alone in an I tag + # - Nested in two I tags + # - "Prime FREE Delivery" in this node + shipping_spans = shipping_node.findall("span") shipping_bs = shipping_node.findall("b") - shipping_is = shipping_node.findall("i") + # shipping_is = shipping_node.findall("i") + shipping_is = shipping_node.xpath("//i[@aria-label]") if len(shipping_spans) > 0: # If the span starts with a "& " it's free shipping (right?) if shipping_spans[0].text.strip() == "&": @@ -1428,10 +1466,13 @@ def get_shipping_costs(tree, free_shipping_string): ) elif len(shipping_is) > 0: # If it has prime icon class, assume free Prime shipping - if "Free" in shipping_is[0].attrib["aria-label"]: + if "FREE" in shipping_is[0].attrib["aria-label"].upper(): log.debug("Found Free shipping with Prime") + elif any(shipping_span_text.upper() in free_message for free_message in amazon_config["FREE_SHIPPING"]): + # We found some version of "free" inside the span.. but this relies on a match + log.warning(f"Assuming free shipping based on this message: '{shipping_span_text}'") else: log.error( - f"Unable to locate price. Assuming 0. Found this: '{shipping_node.text.strip()}'" + f"Unable to locate price. Assuming 0. Found this: '{shipping_span_text}' Consider reporting to #tech-support Discord." ) return FREE_SHIPPING_PRICE From e75f084d8c89cbd5e5b2d693213cb731a5b93880 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 14 Jan 2021 13:12:01 -0500 Subject: [PATCH 031/150] added support for "email me" no stock PDP version updates to selectors for offers on offers page --- stores/amazon.py | 75 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index e94b099a..51eea0e1 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -314,6 +314,21 @@ def login(self): else: log.info("Email not needed.") + if "reverification" in self.driver.current_url: + log.warning( + "Beta code for allowing user to solve OTP. Please report success/failures " + "to #feature-testing on Discord" + ) + # Maybe/Probably/Likely a One Time Password prompt? Let's wait until the user takes action + self.notification_handler.play_alarm_sound() + log.error("One Time Password input required... pausing for user input") + try: + WebDriverWait(self.driver, timeout=300).until( + lambda d: "/ap/" not in d.driver.current_url + ) + except sel_exceptions.TimeoutException: + log.error("User did not solve One Time Password prompt in time.") + if self.driver.find_elements_by_xpath('//*[@id="auth-error-message-box"]'): log.error("Login failed, delete your credentials file") time.sleep(240) @@ -470,17 +485,24 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): lambda d: d.find_element_by_xpath( "//div[@id='aod-offer-list'] | " "//div[@id='olpOfferList'] | " - "//span[@data-action='show-all-offers-display'] |" + "//span[@data-action='show-all-offers-display'] | " + "//div[@id='backInStock' or @id='outOfStock'] |" "//input[@name='submit.add-to-cart' and not(//span[@data-action='show-all-offers-display'])]" ) ) offer_count = [] - if offers_exist.get_attribute("id") == "olpOfferList": + offer_id = offers_exist.get_attribute("id") + if offer_id == "outOfStock" or offer_id == "backInStock": + # No dice... Early out and move on + log.info("Item is currently unavailable. Moving on...") + return False + + if offer_id == "olpOfferList": # Offers Page ... count the 'a-row' classes to know how many offers we 'see' offer_count = self.driver.find_elements_by_xpath( - "//div[contains(@class, 'a-row')]" + "//div[@id='olpOfferList']//div[contains(@class, 'olpOffer')]" ) - elif offers_exist.get_attribute("id") == "aod-offer-list": + elif offer_id == "aod-offer-list": # Offer Flyout or Ajax call ... count the 'aod-offer' divs that we 'see' offer_count = self.driver.find_elements_by_xpath( "//div[@id='aod-offer']" @@ -497,7 +519,9 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): # Now wait for the flyout to load log.info("Waiting for flyout... probably") WebDriverWait(self.driver, timeout=5).until( - lambda d: d.find_element_by_xpath("//div[@id='aod-container']") + lambda d: d.find_element_by_xpath( + "//div[@id='aod-offer-list'] | //div[@id='olpOfferList']" + ) ) log.info("It flew out?!") continue @@ -507,12 +531,16 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): "NOT YET IMPLEMENTED: PDP represents only item worth considering. Parse pricing and Add To Cart from PDP if item qualifies." ) return False + if len(offer_count) == 0: + log.info("No offers found. Moving on.") + return False log.info( f"Found {len(offer_count)} offers in the HTML. Attempting to parse..." ) except sel_exceptions.TimeoutException: log.error("Timed out waiting for offers to render. Skipping...") + log.error(f"URL: {self.driver.current_url}") return False except sel_exceptions.NoSuchElementException: log.error("Unable to find any offers listing. Skipping...") @@ -521,20 +549,20 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): atc_buttons = self.driver.find_elements_by_xpath( '//*[@name="submit.addToCart"]' ) - if not atc_buttons: - # Sanity check to see if we have a valid page, but no offers: - offer_count = WebDriverWait(self.driver, timeout=5).until( - lambda d: d.find_element_by_xpath( - "//div[@id='aod-offer-list']//input[@id='aod-total-offer-count']" - ) - ) - - # offer_count = self.driver.find_element_by_xpath( - # "//div[@id='aod-offer-list']//input[@id='aod-total-offer-count']" - # ) - if offer_count.get_attribute("value") == "0": - log.info("Found zero offers explicitly. Moving to next ASIN.") - return False + # if not atc_buttons: + # # Sanity check to see if we have a valid page, but no offers: + # offer_count = WebDriverWait(self.driver, timeout=25).until( + # lambda d: d.find_element_by_xpath( + # "//div[@id='aod-offer-list']//input[@id='aod-total-offer-count']" + # ) + # ) + # + # # offer_count = self.driver.find_element_by_xpath( + # # "//div[@id='aod-offer-list']//input[@id='aod-total-offer-count']" + # # ) + # if offer_count.get_attribute("value") == "0": + # log.info("Found zero offers explicitly. Moving to next ASIN.") + # return False if atc_buttons: # Early out if we found buttons break @@ -1468,9 +1496,14 @@ def get_shipping_costs(tree, free_shipping_string): # If it has prime icon class, assume free Prime shipping if "FREE" in shipping_is[0].attrib["aria-label"].upper(): log.debug("Found Free shipping with Prime") - elif any(shipping_span_text.upper() in free_message for free_message in amazon_config["FREE_SHIPPING"]): + elif any( + shipping_span_text.upper() in free_message + for free_message in amazon_config["FREE_SHIPPING"] + ): # We found some version of "free" inside the span.. but this relies on a match - log.warning(f"Assuming free shipping based on this message: '{shipping_span_text}'") + log.warning( + f"Assuming free shipping based on this message: '{shipping_span_text}'" + ) else: log.error( f"Unable to locate price. Assuming 0. Found this: '{shipping_span_text}' Consider reporting to #tech-support Discord." From 5880e12765f4bda06c82bda90ad98ecfa72bdf54 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 15 Jan 2021 08:46:59 -0500 Subject: [PATCH 032/150] Updated price parsing when in flyout mode --- stores/amazon.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 51eea0e1..3b7aca83 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -547,7 +547,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False atc_buttons = self.driver.find_elements_by_xpath( - '//*[@name="submit.addToCart"]' + "//div[@id='aod-offer-list' or @id='olpOfferList']//input[@name='submit.addToCart']" ) # if not atc_buttons: # # Sanity check to see if we have a valid page, but no offers: @@ -643,7 +643,10 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): for idx, atc_button in enumerate(atc_buttons): try: - price = parse_price(prices[idx].text) + if flyout_mode: + price = parse_price(prices[idx].get_attribute("innerHTML")) + else: + price = parse_price(prices[idx].text) except IndexError: log.debug("Price index error") return False From 9e920646523f43af8b36ad63908e0258a307f4dd Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 18 Jan 2021 16:12:30 -0500 Subject: [PATCH 033/150] Extended shutdown time for items in cart error to 30 seconds. --- stores/amazon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 3b7aca83..759f7a09 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -183,8 +183,8 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): if cart_quantity > 0: log.warning(f"Found {cart_quantity} item(s) in your cart.") log.info("Delete all item(s) in cart before starting bot.") - log.info("Exiting now...") - time.sleep(5) + log.info("Exiting in 30 seconds...") + time.sleep(30) return self.handle_startup() if not self.is_logged_in(): From fb84f5607d3fbcd4039d509fd63f0679deea2f3e Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 19 Jan 2021 12:09:30 -0500 Subject: [PATCH 034/150] -- Added support for pinned offers -- Added support for manual intervention of known select address pages --- config/fairgame.conf | 3 +++ stores/amazon.py | 44 ++++++++++++++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 99842f4c..6f239bfb 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -117,6 +117,9 @@ "FRI FRAKT", "GRATIS-LIEFERUNG", "PRIME FREE DELIVERY" + ], + "ADDRESS_SELECT": [ + "Select a delivery address" ] } } \ No newline at end of file diff --git a/stores/amazon.py b/stores/amazon.py index 759f7a09..1059efe8 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -103,7 +103,7 @@ def __init__( self.no_image = no_image self.log_stock_check = log_stock_check self.shipping_bypass = shipping_bypass - + self.unknown_title_notification_sent = False presence.enabled = not disable_presence global amazon_config @@ -481,9 +481,11 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): while True: # Sanity check to see if we have any offers try: - offers_exist = WebDriverWait(self.driver, timeout=5).until( + offers_exist = WebDriverWait( + self.driver, timeout=DEFAULT_MAX_TIMEOUT + ).until( lambda d: d.find_element_by_xpath( - "//div[@id='aod-offer-list'] | " + "//div[@id='aod-container'] | " "//div[@id='olpOfferList'] | " "//span[@data-action='show-all-offers-display'] | " "//div[@id='backInStock' or @id='outOfStock'] |" @@ -502,10 +504,10 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): offer_count = self.driver.find_elements_by_xpath( "//div[@id='olpOfferList']//div[contains(@class, 'olpOffer')]" ) - elif offer_id == "aod-offer-list": + elif offer_id == "aod-container": # Offer Flyout or Ajax call ... count the 'aod-offer' divs that we 'see' offer_count = self.driver.find_elements_by_xpath( - "//div[@id='aod-offer']" + "//div[@id='aod-pinned-offer' or @id='aod-offer']" ) elif ( offers_exist.get_attribute("data-action") @@ -518,9 +520,9 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ).click() # Now wait for the flyout to load log.info("Waiting for flyout... probably") - WebDriverWait(self.driver, timeout=5).until( + WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_element_by_xpath( - "//div[@id='aod-offer-list'] | //div[@id='olpOfferList']" + "//div[@id='aod-container'] | //div[@id='olpOfferList']" ) ) log.info("It flew out?!") @@ -547,7 +549,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False atc_buttons = self.driver.find_elements_by_xpath( - "//div[@id='aod-offer-list' or @id='olpOfferList']//input[@name='submit.addToCart']" + "//div[@id='aod-pinned-offer' or @id='aod-offer' or @id='olpOfferList']//input[@name='submit.addToCart']" ) # if not atc_buttons: # # Sanity check to see if we have a valid page, but no offers: @@ -590,7 +592,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): if not prices: # Try the flyout x-paths prices = self.driver.find_elements_by_xpath( - "//div[@id='aod-offer']//div[@id='aod-offer-price']//span[@class='a-price']//span[@class='a-offscreen']" + "//div[@id='aod-pinned-offer' or @id='aod-offer']//div[contains(@id, 'aod-price')]//span[@class='a-price']//span[@class='a-offscreen']" ) if prices: flyout_mode = True @@ -623,7 +625,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): else: # Check for offers offers = self.driver.find_elements_by_xpath( - "//div[@id='aod-offer']" + "//div[@id='aod-pinned-offer' or @id='aod-offer']" ) for idx, offer in enumerate(offers): tree = html.fromstring(offer.get_attribute("innerHTML")) @@ -762,6 +764,24 @@ def navigate_pages(self, test): self.handle_out_of_stock() elif title in amazon_config["BUSINESS_PO_TITLES"]: self.handle_business_po() + elif title in amazon_config["ADDRESS_SELECT"]: + if not self.unknown_title_notification_sent: + self.notification_handler.play_alarm_sound() + self.send_notification( + "User interaction required for checkout!", + title, + self.take_screenshots, + ) + self.unknown_title_notification_sent = True + log.warning( + "Landed on address selection screen. Fairgame will NOT select an address for you. " + "Please select necessary options to arrive at the Review Order Page before the next " + "refresh, or complete checkout manually. You have 30 seconds." + ) + for i in range(30, 0, -1): + log.warning(f"{i}...") + time.sleep(1) + return else: log.debug(f"title is: [{title}]") # see if we can handle blank titles here @@ -863,10 +883,10 @@ def navigate_pages(self, test): # try to handle an unknown title log.error( - f"{title} is not a known title, please create issue indicating the title with a screenshot of page" + f"'{title}' is not a known page title. Please create issue indicating the title with a screenshot of page" ) self.send_notification( - "Encountered Unknown Page Title", + f"Encountered Unknown Page Title: `{title}", "unknown-title", self.take_screenshots, ) From 386993925b2bdd71891c084e9887c37eeb1aae01 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 25 Jan 2021 10:23:01 -0500 Subject: [PATCH 035/150] Updates from user 'antonioh501' on github to accommodate additional page titles on amazon.it and add additional sleeptime between 2FA page title checks --- config/fairgame.conf | 4 +++- stores/amazon.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 6f239bfb..75ca6106 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -68,7 +68,8 @@ "Plaats je bestelling - Amazon.nl-kassa", "Place Your Order - AmazonSmile Checkout", "Preparing your order", - "Ihre Bestellung wird vorbereitet" + "Ihre Bestellung wird vorbereitet", + "Pagamento Amazon.it" ], "ORDER_COMPLETE_TITLES": [ "Amazon.com Thanks You", @@ -93,6 +94,7 @@ "SHIPPING_ONLY_IF": "FREE Shipping on orders over", "TWOFA_TITLES": [ "Two-Step Verification" + "Verifica in due fasi" ], "PRIME_TITLES": [ "Complete your Amazon Prime sign up" diff --git a/stores/amazon.py b/stores/amazon.py index 1059efe8..88a08ea0 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -397,8 +397,9 @@ def login(self): if self.driver.title in amazon_config["TWOFA_TITLES"]: log.info("enter in your two-step verification code in browser") - while self.driver.title in amazon_config["WOFA_TITLES"]: - time.sleep(0.2) + while self.driver.title in amazon_config["TWOFA_TITLES"]: + # Wait for the user to enter 2FA + time.sleep(2) log.info(f'Logged in as {amazon_config["username"]}') @debug From 8c98222535a3b6ff9e20999f098323d62386018d Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 26 Jan 2021 13:49:51 -0500 Subject: [PATCH 036/150] --Added command line option to purge browser profile (--clean-profile) --Added command line option to purge Amazon credentials --Refactored profile name to global option --- cli/cli.py | 49 ++++++++++++++++++++++++++++++++++++++---- common/globalconfig.py | 11 ++++++++++ config/fairgame.conf | 27 ++++++++++++----------- stores/amazon.py | 20 ++++++----------- 4 files changed, 78 insertions(+), 29 deletions(-) diff --git a/cli/cli.py b/cli/cli.py index 0f7145cb..b3b99724 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -1,9 +1,12 @@ -import time -import click +import os +import shutil from datetime import datetime from functools import wraps +from pathlib import Path from signal import signal, SIGINT +import click + try: import click except ModuleNotFoundError as e: @@ -20,12 +23,24 @@ from notifications.notifications import NotificationHandler, TIME_FORMAT from utils.logger import log -from common.globalconfig import GlobalConfig +from common.globalconfig import GlobalConfig, AMAZON_CREDENTIAL_FILE from utils.version import is_latest, version from stores.amazon import Amazon from stores.bestbuy import BestBuyHandler +def get_folder_size(folder): + return sizeof_fmt(sum(file.stat().st_size for file in Path(folder).rglob("*"))) + + +def sizeof_fmt(num, suffix="B"): + for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: + if abs(num) < 1024.0: + return "%3.1f%s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.1f%s%s" % (num, "Yi", suffix) + + def handler(signal, frame): log.info("Caught the stop, exiting.") exit(0) @@ -144,6 +159,18 @@ def main(): default=False, help="Will attempt to click ship to address button. USE AT YOUR OWN RISK!", ) +@click.option( + "--clean-profile", + is_flag=True, + default=False, + help="Purge the user profile that Fairgame uses for browsing", +) +@click.option( + "--clean-credentials", + is_flag=True, + default=False, + help="Purge Amazon credentials and prompt for new credentials", +) @notify_on_crash def amazon( no_image, @@ -161,11 +188,25 @@ def amazon( p, log_stock_check, shipping_bypass, + clean_profile, + clean_credentials, ): notification_handler.sound_enabled = not disable_sound if not notification_handler.sound_enabled: log.info("Local sounds have been disabled.") + if clean_profile and os.path.exists(global_config.get_browser_profile_path()): + log.info( + f"Removing existing profile at '{global_config.get_browser_profile_path()}'" + ) + profile_size = get_folder_size(global_config.get_browser_profile_path()) + shutil.rmtree(global_config.get_browser_profile_path()) + log.info(f"Freed {profile_size}") + + if clean_credentials and os.path.exists(AMAZON_CREDENTIAL_FILE): + log.info(f"Removing existing Amazon credentials from {AMAZON_CREDENTIAL_FILE}") + os.remove(AMAZON_CREDENTIAL_FILE) + amzn_obj = Amazon( headless=headless, notification_handler=notification_handler, @@ -243,7 +284,7 @@ def test_notifications(disable_sound): log.warning(f"FairGame PRE-RELEASE v{version}") else: log.warning( - f"You are running FairGame v{version.release}, but the most recent version is v{remote_version.release}. " + f"You are running FairGame v{version.release}, but the most recent version is v{version.get_latest_version()}. " f"Consider upgrading " ) diff --git a/common/globalconfig.py b/common/globalconfig.py index bca77217..475f33a8 100644 --- a/common/globalconfig.py +++ b/common/globalconfig.py @@ -37,6 +37,9 @@ def __init__(self) -> None: # Load up the global configuration # See http://docs.red-dove.com/cfg/python.html#getting-started-with-cfg-in-python for how to use Config self.global_config = Cfg(GLOBAL_CONFIG_FILE) + self.fairgame_config = self.global_config.get("FAIRGAME") + self.profile_path = None + self.get_browser_profile_path() def get_amazon_config(self, encryption_pass=None): log.info("Initializing Amazon configuration...") @@ -46,3 +49,11 @@ def get_amazon_config(self, encryption_pass=None): AMAZON_CREDENTIAL_FILE, encryption_pass ) return amazon_config + + def get_browser_profile_path(self): + if not self.profile_path: + self.profile_path = os.path.join( + os.path.dirname(os.path.abspath("__file__")), + self.global_config["FAIRGAME"].get("profile_name", ".profile-amz"), + ) + return self.profile_path diff --git a/config/fairgame.conf b/config/fairgame.conf index 75ca6106..9f635173 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -1,4 +1,7 @@ { + "FAIRGAME": { + "profile_name": ".profile-amz" + }, "AMAZON": { "SIGN_IN_TEXT": [ "Hello, Sign in", @@ -108,20 +111,20 @@ "There are currently no listings for this search. Try a different refinement.", "There are currently no listings for this product in . Try changing the condition type." ], - "FREE_SHIPPING":[ - "FREE SHIPPING", - "GRATIS BEZORGING", - "FRETE GRÁTIS", - "LIVRAISON GRATUITE", - "FREE DELIVERY.", - "FREE DELIVERY", - "ENVÍO GRATIS.", - "FRI FRAKT", - "GRATIS-LIEFERUNG", - "PRIME FREE DELIVERY" + "FREE_SHIPPING": [ + "FREE SHIPPING", + "GRATIS BEZORGING", + "FRETE GRÁTIS", + "LIVRAISON GRATUITE", + "FREE DELIVERY.", + "FREE DELIVERY", + "ENVÍO GRATIS.", + "FRI FRAKT", + "GRATIS-LIEFERUNG", + "PRIME FREE DELIVERY" ], "ADDRESS_SELECT": [ - "Select a delivery address" + "Select a delivery address" ] } } \ No newline at end of file diff --git a/stores/amazon.py b/stores/amazon.py index 88a08ea0..3d53731f 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -104,12 +104,14 @@ def __init__( self.log_stock_check = log_stock_check self.shipping_bypass = shipping_bypass self.unknown_title_notification_sent = False + presence.enabled = not disable_presence global amazon_config from cli.cli import global_config amazon_config = global_config.get_amazon_config(encryption_pass) + self.profile_path = global_config.get_browser_profile_path() try: presence.start_presence() @@ -155,7 +157,7 @@ def __init__( ) exit(0) - if not self.create_driver(): + if not self.create_driver(self.profile_path): exit(1) for key in AMAZON_URLS.keys(): @@ -463,7 +465,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): take_screenshot=False, ) raise RuntimeError("Failed to restart bot") - elif not self.create_driver(): + elif not self.create_driver(self.profile_path): log.error("Failed to recreate webdriver processes") log.error("Please restart bot") self.send_notification( @@ -1362,16 +1364,12 @@ def show_config(self): log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") - def create_driver(self): + def create_driver(self, path_to_profile): if self.setup_driver: if self.headless: enable_headless() - # profile_amz = ".profile-amz" - # # keep profile bloat in check - # if os.path.isdir(profile_amz): - # os.remove(profile_amz) prefs = { "profile.password_manager_enabled": False, "credentials_enable_service": False, @@ -1381,9 +1379,6 @@ def create_driver(self): else: prefs["profile.managed_default_content_settings.images"] = 0 options.add_experimental_option("prefs", prefs) - path_to_profile = os.path.join( - os.path.dirname(os.path.abspath("__file__")), ".profile-amz" - ) options.add_argument(f"user-data-dir={path_to_profile}") if not self.slow_mode: options.set_capability("pageLoadStrategy", "none") @@ -1392,8 +1387,7 @@ def create_driver(self): # Delete crashed, so restore pop-up doesn't happen path_to_prefs = os.path.join( - os.path.dirname(os.path.abspath("__file__")), - ".profile-amz", + path_to_profile, "Default", "Preferences", ) @@ -1410,7 +1404,7 @@ def create_driver(self): except Exception as e: log.error(e) log.error( - "If you have a JSON warning above, try deleting your .profile-amz folder" + "If you have a JSON warning above, try cleaning your profile (e.g. --clean-profile)" ) log.error( "If that's not it, you probably have a previous Chrome window open. You should close it." From 97421ccd573c34db393a8b28a01de592c35143d4 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 27 Jan 2021 16:20:06 -0500 Subject: [PATCH 037/150] --Added minimal check to see if the offers flyout was already opened before attempting to click the open offers link. --- stores/amazon.py | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 3d53731f..a5a549f6 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -16,6 +16,7 @@ from selenium import webdriver from selenium.common import exceptions as sel_exceptions from selenium.webdriver.common.keys import Keys +from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.ui import WebDriverWait from utils import discord_presence as presence @@ -516,19 +517,27 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): offers_exist.get_attribute("data-action") == "show-all-offers-display" ): - # No offers to parse... look for a link to the offers - log.info("Attempting to click the open offers link...") - self.driver.find_element_by_xpath( - "//span[@data-action='show-all-offers-display']//a" - ).click() - # Now wait for the flyout to load - log.info("Waiting for flyout... probably") - WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( - lambda d: d.find_element_by_xpath( - "//div[@id='aod-container'] | //div[@id='olpOfferList']" - ) + # Let's double check that the offers display wasn't automatically opened + close_offers_x: WebElement = self.driver.find_element_by_xpath( + "//div[@id='all-offers-display']//i[@aria-label='aod-close']" ) - log.info("It flew out?!") + # TODO: invert this check for production... currently for logging/debugging + if close_offers_x: + log.info(f"Fly-out is already out... keep on going'") + else: + # No offers to parse... look for a link to the offers + log.info("Attempting to click the open offers link...") + self.driver.find_element_by_xpath( + "//span[@data-action='show-all-offers-display']//a" + ).click() + # Now wait for the flyout to load + log.info("Waiting for flyout... probably") + WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( + lambda d: d.find_element_by_xpath( + "//div[@id='aod-container'] | //div[@id='olpOfferList']" + ) + ) + log.info("It flew out?!") continue else: # This assumes we're on a PDP with only an add to cart button... no offers @@ -550,6 +559,21 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): except sel_exceptions.NoSuchElementException: log.error("Unable to find any offers listing. Skipping...") return False + except sel_exceptions.ElementClickInterceptedException as e: + log.error( + "Covering element detected... sleeping to let you inspect. CTRL-C to quit." + ) + log.exception(e) + close_offers_x: WebElement = self.driver.find_element_by_xpath( + "//div[@id='all-offers-display']" + ) + if close_offers_x: + log.info( + f"Is all-offers-display already open? {close_offers_x.is_displayed()}" + ) + self.send_notification("User intervention required", "covered_element") + time.sleep(300) + exit(5) atc_buttons = self.driver.find_elements_by_xpath( "//div[@id='aod-pinned-offer' or @id='aod-offer' or @id='olpOfferList']//input[@name='submit.addToCart']" From 70ae252bc104b2b7c6cb826db445df6a54d43b5f Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 28 Jan 2021 17:58:21 -0500 Subject: [PATCH 038/150] --Updated to handle the auto-flyout event on a redirect from offers page --Turned down logging --Tolerate slow loading flyout --- stores/amazon.py | 103 +++++++++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index a5a549f6..9be719de 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -485,9 +485,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): while True: # Sanity check to see if we have any offers try: - offers_exist = WebDriverWait( - self.driver, timeout=DEFAULT_MAX_TIMEOUT - ).until( + offers = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_element_by_xpath( "//div[@id='aod-container'] | " "//div[@id='olpOfferList'] | " @@ -497,7 +495,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) ) offer_count = [] - offer_id = offers_exist.get_attribute("id") + offer_id = offers.get_attribute("id") if offer_id == "outOfStock" or offer_id == "backInStock": # No dice... Early out and move on log.info("Item is currently unavailable. Moving on...") @@ -513,67 +511,94 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): offer_count = self.driver.find_elements_by_xpath( "//div[@id='aod-pinned-offer' or @id='aod-offer']" ) - elif ( - offers_exist.get_attribute("data-action") - == "show-all-offers-display" - ): - # Let's double check that the offers display wasn't automatically opened - close_offers_x: WebElement = self.driver.find_element_by_xpath( - "//div[@id='all-offers-display']//i[@aria-label='aod-close']" + elif offers.get_attribute("data-action") == "show-all-offers-display": + # PDP Page + # Find the offers link first, just to burn some cycles in case the flyout is loading + open_offers_link: WebElement = self.driver.find_element_by_xpath( + "//span[@data-action='show-all-offers-display']//a" ) - # TODO: invert this check for production... currently for logging/debugging - if close_offers_x: - log.info(f"Fly-out is already out... keep on going'") - else: - # No offers to parse... look for a link to the offers - log.info("Attempting to click the open offers link...") - self.driver.find_element_by_xpath( - "//span[@data-action='show-all-offers-display']//a" - ).click() + + # Now check to see if we're already loading the flyout... + flyout = self.driver.find_elements_by_xpath( + "/html/body/div[@id='all-offers-display']" + ) + if flyout: + # This means we have a flyout already loading, as it gets inserted as the first + # div after the body tag of the document. Wait for the container to load and start + # the loop again to scan for known elements + log.debug( + "Found a loading flyout div. Waiting for offers to load..." + ) + WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( + lambda d: d.find_element_by_xpath( + "//div[@id='aod-container'] " + ) + ) + continue + + log.info("Attempting to click the open offers link...") + open_offers_link.click() + try: # Now wait for the flyout to load - log.info("Waiting for flyout... probably") + log.debug("Waiting for flyout...") WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_element_by_xpath( "//div[@id='aod-container'] | //div[@id='olpOfferList']" ) ) - log.info("It flew out?!") + log.debug("Flyout should be open and populated.") + except sel_exceptions.TimeoutException as te: + log.error( + "Timed out waiting for the flyout to open and populate. Is the " + "connection slow? Do you see the flyout populate?" + ) continue - else: + elif ( + offers.get_attribute("aria-labelledby") + == "submit.add-to-cart-announce" + ): # This assumes we're on a PDP with only an add to cart button... no offers log.warning( - "NOT YET IMPLEMENTED: PDP represents only item worth considering. Parse pricing and Add To Cart from PDP if item qualifies." + "NOT YET IMPLEMENTED: PDP represents only item worth considering. No other sellers available." + " TODO: Parse pricing and Add To Cart from PDP if item qualifies." + ) + else: + log.warning( + "We found elements, but didn't recognize any of the combinations." + ) + log.warning(f"Element found: {offers.tag_name}") + attrs = self.driver.execute_script( + "var items = {}; " + "for (index = 0; index < arguments[0].attributes.length; ++index) " + "{ items[arguments[0].attributes[index].name] = arguments[0].attributes[index].value }; " + "return items;", + offers, ) + log.warning("Dumping element attributes:") + for attr in attrs: + log.warning(f"{attr} = {attrs[attr]}") + return False if len(offer_count) == 0: log.info("No offers found. Moving on.") return False log.info( - f"Found {len(offer_count)} offers in the HTML. Attempting to parse..." + f"Found {len(offer_count)} offers in the HTML. Comparing offers..." ) - except sel_exceptions.TimeoutException: + except sel_exceptions.TimeoutException as te: log.error("Timed out waiting for offers to render. Skipping...") log.error(f"URL: {self.driver.current_url}") + log.exception(te) return False except sel_exceptions.NoSuchElementException: log.error("Unable to find any offers listing. Skipping...") return False except sel_exceptions.ElementClickInterceptedException as e: - log.error( - "Covering element detected... sleeping to let you inspect. CTRL-C to quit." + log.deug( + "Covering element detected... Assuming it's a slow flyout... scanning document again..." ) - log.exception(e) - close_offers_x: WebElement = self.driver.find_element_by_xpath( - "//div[@id='all-offers-display']" - ) - if close_offers_x: - log.info( - f"Is all-offers-display already open? {close_offers_x.is_displayed()}" - ) - self.send_notification("User intervention required", "covered_element") - time.sleep(300) - exit(5) + continue atc_buttons = self.driver.find_elements_by_xpath( "//div[@id='aod-pinned-offer' or @id='aod-offer' or @id='olpOfferList']//input[@name='submit.addToCart']" From 1927446d155b4c4a3e62d7594fbd57060644a53f Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 28 Jan 2021 19:26:34 -0500 Subject: [PATCH 039/150] --corrected logging typo --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index 9be719de..ff0f7e89 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -595,7 +595,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): log.error("Unable to find any offers listing. Skipping...") return False except sel_exceptions.ElementClickInterceptedException as e: - log.deug( + log.debug( "Covering element detected... Assuming it's a slow flyout... scanning document again..." ) continue From efafd2575c2b9c7fedcd9c29ce63ae58fd94e5a7 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 29 Jan 2021 12:53:18 -0500 Subject: [PATCH 040/150] --Added two page titles to the Address Select collection --Fixed formatting of TWOFA_TITLES --- config/fairgame.conf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 9f635173..303b4eae 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -96,7 +96,7 @@ ], "SHIPPING_ONLY_IF": "FREE Shipping on orders over", "TWOFA_TITLES": [ - "Two-Step Verification" + "Two-Step Verification", "Verifica in due fasi" ], "PRIME_TITLES": [ @@ -124,7 +124,9 @@ "PRIME FREE DELIVERY" ], "ADDRESS_SELECT": [ - "Select a delivery address" + "Select a delivery address", + "Ordine in preparazione", + "Select a shipping address" ] } } \ No newline at end of file From 1d24cc48653423935c312d846b0e6ab46437d942 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 30 Jan 2021 16:02:23 -0500 Subject: [PATCH 041/150] --Forced wait for footer to load before scanning page for pricing --Upgraded to ChromeDriver = 88.0.4324.96 --- Pipfile | 2 +- stores/amazon.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index f3abd491..931e2fcb 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pyinstaller = "*" requests = "==2.24.0" click = "*" selenium = "*" -chromedriver-py = "==87.0.4280.88" +chromedriver-py = "==88.0.4324.96" furl = "*" twilio = "*" discord-webhook = "*" diff --git a/stores/amazon.py b/stores/amazon.py index ff0f7e89..e93864ed 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -485,6 +485,13 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): while True: # Sanity check to see if we have any offers try: + # Wait for the page to load before determining what's in it by looking for the footer + footer = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( + lambda d: d.find_elements_by_xpath( + "//div[@class='nav-footer-line']" + ) + ) + offers = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_element_by_xpath( "//div[@id='aod-container'] | " From 61900c2cd506acf9d4a5debab53b4bafda1fed7c Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 30 Jan 2021 16:20:18 -0500 Subject: [PATCH 042/150] --Updated version to distinguish between feature and mainline development --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 8fc65b24..1f295850 100644 --- a/utils/version.py +++ b/utils/version.py @@ -8,7 +8,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev1" +__VERSION = "0.6.0.dev2" version = Version(__VERSION) From 9d0e62fc80de37d357081c2b85cbca01ce0d6208 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 30 Jan 2021 17:41:58 -0500 Subject: [PATCH 043/150] --Added the concept of Amazon Item Condition for determining if an item is used or new --Added additional parsing to pull out item condition from flyout --- stores/amazon.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index e93864ed..3b32a8a6 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -5,6 +5,7 @@ import platform import time from datetime import datetime +from enum import Enum import psutil from amazoncaptcha import AmazonCaptcha @@ -90,6 +91,10 @@ def __init__( self.button_xpaths = BUTTON_XPATHS self.detailed = detailed self.used = used + if used: + self.condition = AmazonItemCondition.UsedAcceptable + else: + self.condition = AmazonItemCondition.New self.single_shot = single_shot self.take_screenshots = not no_screenshots self.start_time = time.time() @@ -607,7 +612,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) continue - atc_buttons = self.driver.find_elements_by_xpath( + atc_buttons: WebElement = self.driver.find_elements_by_xpath( "//div[@id='aod-pinned-offer' or @id='aod-offer' or @id='olpOfferList']//input[@name='submit.addToCart']" ) # if not atc_buttons: @@ -703,6 +708,24 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): log.debug(f"\tShipping Price: {shipping_price}") for idx, atc_button in enumerate(atc_buttons): + # Condition check first, using the button to find the form that will divulge the item's condition + if flyout_mode: + condition: WebElement = atc_button.find_elements_by_xpath( + "./ancestor::form[@method='post']" + ) + + if condition: + atc_form_action = condition[0].get_attribute("action") + item_condition = get_item_condition(atc_form_action) + # Lower condition value imply newer + if item_condition.value > self.condition.value: + # Item is below our standards, so skip it + log.debug( + f"Skipping item because its condition is below the requested level: " + f"{item_condition} is below {self.condition}" + ) + continue + try: if flyout_mode: price = parse_price(prices[idx].get_attribute("innerHTML")) @@ -1583,3 +1606,53 @@ def get_shipping_costs(tree, free_shipping_string): f"Unable to locate price. Assuming 0. Found this: '{shipping_span_text}' Consider reporting to #tech-support Discord." ) return FREE_SHIPPING_PRICE + + +class AmazonItemCondition(Enum): + # See https://sellercentral.amazon.com/gp/help/external/200386310?language=en_US&ref=efph_200386310_cont_G1831 + New = 10 + Renewed = 20 + Refurbished = 20 + Rental = 30 + Open_box = 40 + UsedLikeNew = 40 + UsedVeryGood = 50 + UsedGood = 60 + UsedAcceptable = 70 + CollectibleLikeNew = 40 + CollectibleVeryGood = 50 + CollectibleGood = 60 + CollectibleAcceptable = 70 + Unknown = 1000 + + @classmethod + def from_str(cls, label): + # Straight lookup + try: + condition = AmazonItemCondition[label] + return condition + except KeyError: + # Key doesn't exist as a Member, so try cleaning up the string + cleaned_label = "".join(label.split()) + cleaned_label = cleaned_label.replace("-", "") + try: + condition = AmazonItemCondition[cleaned_label] + return condition + except KeyError: + raise NotImplementedError + + +def get_item_condition(form_action) -> AmazonItemCondition: + """ Attempts to determine the Item Condition from the Add To Cart form action """ + if "_new_" in form_action: + # log.debug(f"Item condition is new") + return AmazonItemCondition.New + elif "_used_" in form_action: + # log.debug(f"Item condition is used") + return AmazonItemCondition.UsedGood + elif "_col_" in form_action: + # og.debug(f"Item condition is collectible") + return AmazonItemCondition.CollectibleGood + else: + # log.debug(f"Item condition is unknown: {form_action}") + return AmazonItemCondition.Unknown From 1e83db41ce6fb3fc7298c5bb0921824d6bf550f1 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 30 Jan 2021 19:41:45 -0500 Subject: [PATCH 044/150] --Updated xpath for detecting offers for the scenario that the flyout opens automatically and there are no offers. --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index e93864ed..a18ba76a 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -516,7 +516,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): elif offer_id == "aod-container": # Offer Flyout or Ajax call ... count the 'aod-offer' divs that we 'see' offer_count = self.driver.find_elements_by_xpath( - "//div[@id='aod-pinned-offer' or @id='aod-offer']" + "//div[@id='aod-pinned-offer' or @id='aod-offer']//input[@name='submit.addToCart']" ) elif offers.get_attribute("data-action") == "show-all-offers-display": # PDP Page From 0cb9e2a5ee829ca79e9919cd6ee3691810606ed1 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 30 Jan 2021 17:41:58 -0500 Subject: [PATCH 045/150] --Added the concept of Amazon Item Condition for determining if an item is used or new --Added additional parsing to pull out item condition from flyout --- stores/amazon.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index a18ba76a..5a4d5cee 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -5,6 +5,7 @@ import platform import time from datetime import datetime +from enum import Enum import psutil from amazoncaptcha import AmazonCaptcha @@ -90,6 +91,10 @@ def __init__( self.button_xpaths = BUTTON_XPATHS self.detailed = detailed self.used = used + if used: + self.condition = AmazonItemCondition.UsedAcceptable + else: + self.condition = AmazonItemCondition.New self.single_shot = single_shot self.take_screenshots = not no_screenshots self.start_time = time.time() @@ -607,7 +612,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) continue - atc_buttons = self.driver.find_elements_by_xpath( + atc_buttons: WebElement = self.driver.find_elements_by_xpath( "//div[@id='aod-pinned-offer' or @id='aod-offer' or @id='olpOfferList']//input[@name='submit.addToCart']" ) # if not atc_buttons: @@ -703,6 +708,24 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): log.debug(f"\tShipping Price: {shipping_price}") for idx, atc_button in enumerate(atc_buttons): + # Condition check first, using the button to find the form that will divulge the item's condition + if flyout_mode: + condition: WebElement = atc_button.find_elements_by_xpath( + "./ancestor::form[@method='post']" + ) + + if condition: + atc_form_action = condition[0].get_attribute("action") + item_condition = get_item_condition(atc_form_action) + # Lower condition value imply newer + if item_condition.value > self.condition.value: + # Item is below our standards, so skip it + log.debug( + f"Skipping item because its condition is below the requested level: " + f"{item_condition} is below {self.condition}" + ) + continue + try: if flyout_mode: price = parse_price(prices[idx].get_attribute("innerHTML")) @@ -1583,3 +1606,53 @@ def get_shipping_costs(tree, free_shipping_string): f"Unable to locate price. Assuming 0. Found this: '{shipping_span_text}' Consider reporting to #tech-support Discord." ) return FREE_SHIPPING_PRICE + + +class AmazonItemCondition(Enum): + # See https://sellercentral.amazon.com/gp/help/external/200386310?language=en_US&ref=efph_200386310_cont_G1831 + New = 10 + Renewed = 20 + Refurbished = 20 + Rental = 30 + Open_box = 40 + UsedLikeNew = 40 + UsedVeryGood = 50 + UsedGood = 60 + UsedAcceptable = 70 + CollectibleLikeNew = 40 + CollectibleVeryGood = 50 + CollectibleGood = 60 + CollectibleAcceptable = 70 + Unknown = 1000 + + @classmethod + def from_str(cls, label): + # Straight lookup + try: + condition = AmazonItemCondition[label] + return condition + except KeyError: + # Key doesn't exist as a Member, so try cleaning up the string + cleaned_label = "".join(label.split()) + cleaned_label = cleaned_label.replace("-", "") + try: + condition = AmazonItemCondition[cleaned_label] + return condition + except KeyError: + raise NotImplementedError + + +def get_item_condition(form_action) -> AmazonItemCondition: + """ Attempts to determine the Item Condition from the Add To Cart form action """ + if "_new_" in form_action: + # log.debug(f"Item condition is new") + return AmazonItemCondition.New + elif "_used_" in form_action: + # log.debug(f"Item condition is used") + return AmazonItemCondition.UsedGood + elif "_col_" in form_action: + # og.debug(f"Item condition is collectible") + return AmazonItemCondition.CollectibleGood + else: + # log.debug(f"Item condition is unknown: {form_action}") + return AmazonItemCondition.Unknown From d2709fe36253d1e42e8b26bfb9c0798cce5c71fe Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sun, 31 Jan 2021 14:36:56 -0500 Subject: [PATCH 046/150] --Added --alt-offers option to force Fairgame to search the product page flyout instead of traversing the offers page redirect. --- cli/cli.py | 8 ++++++++ stores/amazon.py | 28 ++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/cli/cli.py b/cli/cli.py index b3b99724..1954c431 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -171,6 +171,12 @@ def main(): default=False, help="Purge Amazon credentials and prompt for new credentials", ) +@click.option( + "--alt-offers", + is_flag=True, + default=False, + help="Directly hit the PDP for offers. Sub-optimal if you have the offers listing available to you.", +) @notify_on_crash def amazon( no_image, @@ -190,6 +196,7 @@ def amazon( shipping_bypass, clean_profile, clean_credentials, + alt_offers, ): notification_handler.sound_enabled = not disable_sound if not notification_handler.sound_enabled: @@ -221,6 +228,7 @@ def amazon( encryption_pass=p, log_stock_check=log_stock_check, shipping_bypass=shipping_bypass, + alt_offers=alt_offers, ) try: amzn_obj.run(delay=delay, test=test) diff --git a/stores/amazon.py b/stores/amazon.py index 5a4d5cee..010e5219 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -29,6 +29,7 @@ AMAZON_URLS = { "BASE_URL": "https://{domain}/", "OFFER_URL": "https://{domain}/gp/offer-listing/", + "ALT_OFFER_URL": "https://{domain}/dp/", "CART_URL": "https://{domain}/gp/cart/view.html", } CHECKOUT_URL = "https://{domain}/gp/cart/desktop/go-to-checkout.html/ref=ox_sc_proceed?partialCheckoutCart=1&isToBeGiftWrappedBefore=0&proceedToRetailCheckout=Proceed+to+checkout&proceedToCheckout=1&cartInitiateId={cart_id}" @@ -82,6 +83,7 @@ def __init__( encryption_pass=None, log_stock_check=False, shipping_bypass=False, + alt_offers=False, ): self.notification_handler = notification_handler self.asin_list = [] @@ -168,6 +170,11 @@ def __init__( for key in AMAZON_URLS.keys(): AMAZON_URLS[key] = AMAZON_URLS[key].format(domain=self.amazon_website) + if alt_offers: + log.info("Using alternate page for offer parsing.") + self.ACTIVE_OFFER_URL = AMAZON_URLS["ALT_OFFER_URL"] + else: + self.ACTIVE_OFFER_URL = AMAZON_URLS["OFFER_URL"] def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): self.testing = test @@ -431,15 +438,15 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False if self.checkshipping: if self.used: - f = furl(AMAZON_URLS["OFFER_URL"] + asin) + f = furl(self.ACTIVE_OFFER_URL + asin) else: - f = furl(AMAZON_URLS["OFFER_URL"] + asin + "/ref=olp_f_new&f_new=true") + f = furl(self.ACTIVE_OFFER_URL + asin + "/ref=olp_f_new&f_new=true") else: if self.used: - f = furl(AMAZON_URLS["OFFER_URL"] + asin + "/f_freeShipping=on") + f = furl(self.ACTIVE_OFFER_URL + asin + "/f_freeShipping=on") else: f = furl( - AMAZON_URLS["OFFER_URL"] + self.ACTIVE_OFFER_URL + asin + "/ref=olp_f_new&f_new=true&f_freeShipping=on" ) @@ -449,6 +456,8 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): while True: try: self.get_page(f.url) + log.debug(f"Initial page title {self.driver.title}") + log.debug(f" page url: {self.driver.current_url}") break except Exception: fail_counter += 1 @@ -497,6 +506,9 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) ) + log.debug(f"After footer page title {self.driver.title}") + log.debug(f" page url: {self.driver.current_url}") + offers = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_element_by_xpath( "//div[@id='aod-container'] | " @@ -1431,14 +1443,14 @@ def show_config(self): log.info( f"--Looking for {len(asins)} ASINs between {self.reserve_min[idx]:.2f} and {self.reserve_max[idx]:.2f}" ) + if not presence.enabled: + log.info(f"--Discord Presence feature is disabled.") if self.no_image: log.info(f"--No images will be requested") if not self.notification_handler.sound_enabled: log.info(f"--Notification sounds are disabled.") - if self.headless: - log.warning( - f"--Running headless is unsupported. If you get it to work, please let us know on Discord." - ) + if self.ACTIVE_OFFER_URL == AMAZON_URLS["ALT_OFFER_URL"]: + log.info(f"--Using alternate offers URL") if self.testing: log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") From 9b5f93b3a989211bd79bf679af3529cd01aca8fa Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Sun, 31 Jan 2021 21:51:42 -0500 Subject: [PATCH 047/150] add copyright and license info (#478) * add copyright and license info * copyright/license: moved some things around moved startup text to app.py * license check added license check and license file hash --- app.py | 50 +++++++++++ cli/__init__.py | 18 ++++ cli/cli.py | 66 +++++++++++++-- cli/license/show_c.txt | 148 +++++++++++++++++++++++++++++++++ cli/license/show_w.txt | 32 +++++++ cli/utils.py | 19 +++++ common/__init__.py | 18 ++++ common/globalconfig.py | 19 +++++ common/license_hash.py | 1 + notifications/__init__.py | 18 ++++ notifications/notifications.py | 19 +++++ stores/__init__.py | 18 ++++ stores/amazon.py | 19 +++++ stores/bestbuy.py | 19 +++++ stores/nvidia.py | 19 +++++ utils/__init__.py | 18 ++++ utils/debugger.py | 19 +++++ utils/discord_presence.py | 19 +++++ utils/encryption.py | 19 +++++ utils/http.py | 19 +++++ utils/json_utils.py | 19 +++++ utils/logger.py | 19 +++++ utils/selenium_utils.py | 19 +++++ utils/version.py | 19 +++++ 24 files changed, 645 insertions(+), 8 deletions(-) create mode 100644 cli/license/show_c.txt create mode 100644 cli/license/show_w.txt create mode 100644 common/license_hash.py diff --git a/app.py b/app.py index f981c6b2..4f6f7774 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,53 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame +import os +import hashlib +from common.license_hash import license_hash + + +def sha256sum(filename): + h = hashlib.sha256() + b = bytearray(128 * 1024) + mv = memoryview(b) + with open(filename, "rb", buffering=0) as f: + for n in iter(lambda: f.readinto(mv), 0): + h.update(mv[:n]) + return h.hexdigest() + + +if os.path.exists("LICENSE") and sha256sum("LICENSE") == license_hash: + s = """ + FairGame Copyright (C) 2021 Hari Nagarajan + This program comes with ABSOLUTELY NO WARRANTY; for details + start the program with the `show --w' option. + + This is free software, and you are welcome to redistribute it + under certain conditions; for details start the program with + the `show --c' option.\n + """ + + print(s) +else: + print("License File Changed or Missing. Quitting Program.") + exit(0) + + from cli import cli diff --git a/cli/__init__.py b/cli/__init__.py index e69de29b..c5e3c3e3 100644 --- a/cli/__init__.py +++ b/cli/__init__.py @@ -0,0 +1,18 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame diff --git a/cli/cli.py b/cli/cli.py index b3b99724..1a0870a8 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import os import shutil from datetime import datetime @@ -5,22 +24,21 @@ from pathlib import Path from signal import signal, SIGINT -import click +LICENSE_PATH = os.path.join( + "cli", + "license", +) + try: import click except ModuleNotFoundError as e: print(e) - print( - "You should try running pipenv shell and pipenv install per the install instructions" - ) - print("Or you should only use Python 3.8.X per the instructions.") - print( - "If you are attempting to run multiple bots, this is not supported, so you are on your own to figure out that one." - ) + print("Install the missing module noted above.") exit(0) import time + from notifications.notifications import NotificationHandler, TIME_FORMAT from utils.logger import log from common.globalconfig import GlobalConfig, AMAZON_CREDENTIAL_FILE @@ -62,6 +80,7 @@ def decorator(*args, **kwargs): @click.group() def main(): + pass @@ -271,11 +290,42 @@ def test_notifications(disable_sound): time.sleep(5) +@click.command() +@click.option("--w", is_flag=True) +@click.option("--c", is_flag=True) +def show(w, c): + if w and c: + print("Choose one option. Program Quitting") + exit(0) + elif w: + show_file = "show_w.txt" + elif c: + show_file = "show_c.txt" + else: + print( + "Option missing, you must include w or c with show argument. Program Quitting" + ) + exit(0) + + if os.path.exists(LICENSE_PATH): + + with open(os.path.join(LICENSE_PATH, show_file)) as file: + try: + print(file.read()) + except FileNotFoundError: + log.error("License File Missing. Quitting Program") + exit(0) + else: + log.error("License File Missing. Quitting Program.") + exit(0) + + signal(SIGINT, handler) main.add_command(amazon) main.add_command(bestbuy) main.add_command(test_notifications) +main.add_command(show) # Global scope stuff here if is_latest(): diff --git a/cli/license/show_c.txt b/cli/license/show_c.txt new file mode 100644 index 00000000..642368ec --- /dev/null +++ b/cli/license/show_c.txt @@ -0,0 +1,148 @@ + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + diff --git a/cli/license/show_w.txt b/cli/license/show_w.txt new file mode 100644 index 00000000..a4e541ef --- /dev/null +++ b/cli/license/show_w.txt @@ -0,0 +1,32 @@ + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + diff --git a/cli/utils.py b/cli/utils.py index 4cbaca48..1b474cc2 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import click import questionary diff --git a/common/__init__.py b/common/__init__.py index e69de29b..c5e3c3e3 100644 --- a/common/__init__.py +++ b/common/__init__.py @@ -0,0 +1,18 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame diff --git a/common/globalconfig.py b/common/globalconfig.py index 475f33a8..5b021988 100644 --- a/common/globalconfig.py +++ b/common/globalconfig.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import os from config import Config as Cfg import stdiomask diff --git a/common/license_hash.py b/common/license_hash.py new file mode 100644 index 00000000..139a592b --- /dev/null +++ b/common/license_hash.py @@ -0,0 +1 @@ +license_hash = "81cbae84a29ce7e770bf2bc7b178e50bda0ce8de6067aba661b0bc7b05b562f8" diff --git a/notifications/__init__.py b/notifications/__init__.py index e69de29b..c5e3c3e3 100644 --- a/notifications/__init__.py +++ b/notifications/__init__.py @@ -0,0 +1,18 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame diff --git a/notifications/notifications.py b/notifications/notifications.py index 86f4be2f..eae7540a 100644 --- a/notifications/notifications.py +++ b/notifications/notifications.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import queue import threading from os import path diff --git a/stores/__init__.py b/stores/__init__.py index e69de29b..c5e3c3e3 100644 --- a/stores/__init__.py +++ b/stores/__init__.py @@ -0,0 +1,18 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame diff --git a/stores/amazon.py b/stores/amazon.py index a18ba76a..49df95ef 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import fileinput import json import math diff --git a/stores/bestbuy.py b/stores/bestbuy.py index cff26cda..a0a7cddd 100644 --- a/stores/bestbuy.py +++ b/stores/bestbuy.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import json import webbrowser from time import sleep diff --git a/stores/nvidia.py b/stores/nvidia.py index eefd3b9e..7ac1137b 100644 --- a/stores/nvidia.py +++ b/stores/nvidia.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import concurrent import json import webbrowser diff --git a/utils/__init__.py b/utils/__init__.py index e69de29b..c5e3c3e3 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -0,0 +1,18 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame diff --git a/utils/debugger.py b/utils/debugger.py index fc7dabab..d17ba596 100644 --- a/utils/debugger.py +++ b/utils/debugger.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + from utils.logger import log import functools diff --git a/utils/discord_presence.py b/utils/discord_presence.py index 202d5a80..172adf53 100644 --- a/utils/discord_presence.py +++ b/utils/discord_presence.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import time from pypresence import Presence diff --git a/utils/encryption.py b/utils/encryption.py index a9b20c55..92516cda 100644 --- a/utils/encryption.py +++ b/utils/encryption.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import getpass as getpass import stdiomask import json diff --git a/utils/http.py b/utils/http.py index ab565bbb..225d45a7 100644 --- a/utils/http.py +++ b/utils/http.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry diff --git a/utils/json_utils.py b/utils/json_utils.py index 7ce9188f..fd5b58df 100644 --- a/utils/json_utils.py +++ b/utils/json_utils.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import json diff --git a/utils/logger.py b/utils/logger.py index efd62096..801a00ae 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import coloredlogs import logging import os diff --git a/utils/selenium_utils.py b/utils/selenium_utils.py index 2dd62bb1..d7121fa6 100644 --- a/utils/selenium_utils.py +++ b/utils/selenium_utils.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import requests from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.action_chains import ActionChains diff --git a/utils/version.py b/utils/version.py index 1f295850..6d1e1928 100644 --- a/utils/version.py +++ b/utils/version.py @@ -1,3 +1,22 @@ +# FairGame - Automated Purchasing Program +# Copyright (C) 2021 Hari Nagarajan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The author may be contacted through the project's GitHub, at: +# https://github.com/Hari-Nagarajan/fairgame + import requests from packaging.version import Version, parse, InvalidVersion From be74fdff53135130a9ef66c88c13abc5a8ff205e Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 1 Feb 2021 09:57:50 -0500 Subject: [PATCH 048/150] --Updated version to reflect Copyright and License changes --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 6d1e1928..8418fd81 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev2" +__VERSION = "0.6.0.dev3" version = Version(__VERSION) From e97442391f53d7e6df427bdae8a6c8c972d647d9 Mon Sep 17 00:00:00 2001 From: Crasoum <44730377+Crasoum@users.noreply.github.com> Date: Mon, 1 Feb 2021 11:26:37 -0600 Subject: [PATCH 049/150] Update README.md --- README.md | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d8884862..807b28a8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Fairgame -[Installation](#Installation) | [Usage](#Usage) | [Discord](https://discord.gg/4rfbNKrmnC) -| [Troubleshooting](#Troubleshooting) +[Installation](#Installation) | [Usage](#Usage) | [Discord](https://discord.gg/4rfbNKrmnC) | [Troubleshooting](#Troubleshooting) ## Why??? @@ -142,12 +141,27 @@ Edit the newly created files with your settings based on your [configuration](#c This is an abridged version of the community created document by UnidentifiedWarlock and Judarius. It can be found [here](https://docs.google.com/document/d/1VUxXhATZ8sZOJxdh3AIY6OGqwLRmrAcPikKZAwphIE8/edit). If the steps here don't work on your Pi 4, look there for additional options. This hasn't been tested on a Pi 3, but given enough RAM to -run Chrome, it may very well work. Let us know. +run Chrome, it may very well work. Let us know. ```shell sudo apt update sudo apt upgrade +sudo apt-get install -y build-essential tk-dev libncurses5-dev libncursesw5-dev libreadline6-dev libdb5.3-dev libgdbm-dev libsqlite3-dev libssl-dev libbz2-dev libexpat1-dev liblzma-dev zlib1g-dev libffi-dev libzbar-dev clang + +version=3.8.7 + +wget https://www.python.org/ftp/python/$version/Python-$version.tgz + +tar zxf Python-$version.tgz +cd Python-$version +./configure --enable-optimizations +make -j4 +sudo make altinstall + +sudo python3 -m pip install --upgrade pip + sudo apt install chromium-chromedriver +cp /usr/bin/chromedriver /home/fairgame/.local/share/virtualenvs/fairgame-/lib/python3.8/site-packages/chromedriver_py/chromedriver_linux64 git clone https://github.com/Hari-Nagarajan/fairgame cd fairgame/ pip3 install pipenv @@ -160,7 +174,7 @@ Leave this Terminal window open. Open the following file in a text editor: -`/home/$USER/.local/share/virtualenvs/fairgame-/lib/python3.7/site-packages/selenium/webdriver/common/service.py` +`/home/$USER/.local/share/virtualenvs/fairgame-/lib/python3.8/site-packages/selenium/webdriver/common/service.py` Edit line 38 from @@ -172,17 +186,6 @@ to Then save and close the file. -Additional steps outside of the readme: - -If you get a compiler error when doing pipenv install run: - -```shell -sudo python3 -m pip install --upgrade pip -sudo apt-get install libffi-dev -sudo apt-get install libzbar-dev -sudo apt-get install clang -y -``` - ## Usage ### Amazon @@ -359,7 +362,7 @@ python app.py amazon --test ... 2020-12-23 13:07:38 INFO Initializing Apprise handler using: config/apprise.conf 2020-12-23 13:07:38 INFO Found Discord configuration -2020-12-23 13:07:38 INFO FairGame v0.5.0 +2020-12-23 13:07:38 INFO FairGame v0.5.4 2020-12-23 13:07:38 INFO Reading credentials from: config/amazon_credentials.json 2020-12-23 13:07:43 INFO ================================================== 2020-12-23 13:07:43 INFO Starting Amazon ASIN Hunt for 2 Products with: From 7ccf5ddd75c0dddcdaba738aea8ba0e73a4f209f Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 1 Feb 2021 14:30:22 -0500 Subject: [PATCH 050/150] --Updated to include both Unix and Windows line feed hashes for the License --- app.py | 3 +-- common/license_hash.py | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 4f6f7774..9fa07dcd 100644 --- a/app.py +++ b/app.py @@ -30,8 +30,7 @@ def sha256sum(filename): h.update(mv[:n]) return h.hexdigest() - -if os.path.exists("LICENSE") and sha256sum("LICENSE") == license_hash: +if os.path.exists("LICENSE") and sha256sum("LICENSE") in license_hash: s = """ FairGame Copyright (C) 2021 Hari Nagarajan This program comes with ABSOLUTELY NO WARRANTY; for details diff --git a/common/license_hash.py b/common/license_hash.py index 139a592b..4d84b097 100644 --- a/common/license_hash.py +++ b/common/license_hash.py @@ -1 +1,4 @@ -license_hash = "81cbae84a29ce7e770bf2bc7b178e50bda0ce8de6067aba661b0bc7b05b562f8" +license_hash = [ + "81cbae84a29ce7e770bf2bc7b178e50bda0ce8de6067aba661b0bc7b05b562f8", + "8b1ba204bb69a0ade2bfcf65ef294a920f6bb361b317dba43c7ef29d96332b9b", +] From a6d865ba0fce7216283ed7241a0771328590cc2d Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 2 Feb 2021 09:05:05 -0500 Subject: [PATCH 051/150] --Hot fix to quickly skip doggo page if we detect it --- stores/amazon.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 8a6d6d7a..3bd26d86 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -519,11 +519,14 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): # Sanity check to see if we have any offers try: # Wait for the page to load before determining what's in it by looking for the footer - footer = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( + footer: WebElement = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_elements_by_xpath( - "//div[@class='nav-footer-line']" + "//div[@class='nav-footer-line'] | //img[@alt='Dogs of Amazon']" ) ) + if footer and footer[0].tag_name == "img": + log.info(f"Saw dogs for {asin}. Skipping...") + return False log.debug(f"After footer page title {self.driver.title}") log.debug(f" page url: {self.driver.current_url}") From 749c61383c6b4b47363715ba989819a8b13dc664 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 2 Feb 2021 09:22:36 -0500 Subject: [PATCH 052/150] --Hot fix to quickly skip doggo page if we detect it --- stores/amazon.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 49df95ef..6c1595fc 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -505,11 +505,14 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): # Sanity check to see if we have any offers try: # Wait for the page to load before determining what's in it by looking for the footer - footer = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( + footer: WebElement = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_elements_by_xpath( - "//div[@class='nav-footer-line']" + "//div[@class='nav-footer-line'] | //img[@alt='Dogs of Amazon']" ) ) + if footer and footer[0].tag_name == "img": + log.info(f"Saw dogs for {asin}. Skipping...") + return False offers = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_element_by_xpath( From 69666619c7f978a1c7f5ea2b63e9b44953087c3a Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Tue, 2 Feb 2021 17:32:33 -0500 Subject: [PATCH 053/150] Update cli.py fixed headless help text --- cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cli.py b/cli/cli.py index 1a0870a8..4c255a45 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -113,7 +113,7 @@ def main(): @click.command() @click.option("--no-image", is_flag=True, help="Do not load images") -@click.option("--headless", is_flag=True, help="Unsupported headless mode. GLHF") +@click.option("--headless", is_flag=True, help="Headless mode.") @click.option( "--test", is_flag=True, From 193a1837188403b9a40293ad0c2df9607217e51f Mon Sep 17 00:00:00 2001 From: unapproachable Date: Tue, 2 Feb 2021 17:33:02 -0500 Subject: [PATCH 054/150] -- Updated to prefer PDP offers page. --alt-offers now attempts to go to the deprecated offer listing page -- Updated to force flyout on page load by default -- Updated logging to be quieter. -- Defaulted to fast page scanning. Can lead to false reports that the PDP is the only page that an offer. --slow-mode to correct for that --- stores/amazon.py | 64 ++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 3bd26d86..628984f7 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -47,8 +47,8 @@ # Optional OFFER_URL is: "OFFER_URL": "https://{domain}/dp/", AMAZON_URLS = { "BASE_URL": "https://{domain}/", - "OFFER_URL": "https://{domain}/gp/offer-listing/", - "ALT_OFFER_URL": "https://{domain}/dp/", + "ALT_OFFER_URL": "https://{domain}/gp/offer-listing/", + "OFFER_URL": "https://{domain}/dp/", "CART_URL": "https://{domain}/gp/cart/view.html", } CHECKOUT_URL = "https://{domain}/gp/cart/desktop/go-to-checkout.html/ref=ox_sc_proceed?partialCheckoutCart=1&isToBeGiftWrappedBefore=0&proceedToRetailCheckout=Proceed+to+checkout&proceedToCheckout=1&cartInitiateId={cart_id}" @@ -131,6 +131,7 @@ def __init__( self.log_stock_check = log_stock_check self.shipping_bypass = shipping_bypass self.unknown_title_notification_sent = False + self.alt_offers = alt_offers presence.enabled = not disable_presence @@ -189,7 +190,7 @@ def __init__( for key in AMAZON_URLS.keys(): AMAZON_URLS[key] = AMAZON_URLS[key].format(domain=self.amazon_website) - if alt_offers: + if self.alt_offers: log.info("Using alternate page for offer parsing.") self.ACTIVE_OFFER_URL = AMAZON_URLS["ALT_OFFER_URL"] else: @@ -455,20 +456,25 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): if retry > DEFAULT_MAX_ATC_TRIES: log.info("max add to cart retries hit, returning to asin check") return False - if self.checkshipping: - if self.used: - f = furl(self.ACTIVE_OFFER_URL + asin) + + if self.alt_offers: + if self.checkshipping: + if self.used: + f = furl(self.ACTIVE_OFFER_URL + asin) + else: + f = furl(self.ACTIVE_OFFER_URL + asin + "/ref=olp_f_new&f_new=true") else: - f = furl(self.ACTIVE_OFFER_URL + asin + "/ref=olp_f_new&f_new=true") + if self.used: + f = furl(self.ACTIVE_OFFER_URL + asin + "/f_freeShipping=on") + else: + f = furl( + self.ACTIVE_OFFER_URL + + asin + + "/ref=olp_f_new&f_new=true&f_freeShipping=on" + ) else: - if self.used: - f = furl(self.ACTIVE_OFFER_URL + asin + "/f_freeShipping=on") - else: - f = furl( - self.ACTIVE_OFFER_URL - + asin - + "/ref=olp_f_new&f_new=true&f_freeShipping=on" - ) + # Force the flyout by default + f = furl(self.ACTIVE_OFFER_URL + asin + "/#aod") fail_counter = 0 presence.searching_update() @@ -519,24 +525,27 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): # Sanity check to see if we have any offers try: # Wait for the page to load before determining what's in it by looking for the footer - footer: WebElement = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( - lambda d: d.find_elements_by_xpath( - "//div[@class='nav-footer-line'] | //img[@alt='Dogs of Amazon']" + if self.slow_mode: + footer: WebElement = WebDriverWait( + self.driver, timeout=DEFAULT_MAX_TIMEOUT + ).until( + lambda d: d.find_elements_by_xpath( + "//div[@class='nav-footer-line'] | //img[@alt='Dogs of Amazon']" + ) ) - ) - if footer and footer[0].tag_name == "img": - log.info(f"Saw dogs for {asin}. Skipping...") - return False + if footer and footer[0].tag_name == "img": + log.info(f"Saw dogs for {asin}. Skipping...") + return False - log.debug(f"After footer page title {self.driver.title}") - log.debug(f" page url: {self.driver.current_url}") + log.debug(f"After footer page title {self.driver.title}") + log.debug(f" page url: {self.driver.current_url}") offers = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_element_by_xpath( "//div[@id='aod-container'] | " "//div[@id='olpOfferList'] | " - "//span[@data-action='show-all-offers-display'] | " "//div[@id='backInStock' or @id='outOfStock'] |" + "//span[@data-action='show-all-offers-display'] | " "//input[@name='submit.add-to-cart' and not(//span[@data-action='show-all-offers-display'])]" ) ) @@ -582,7 +591,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) continue - log.info("Attempting to click the open offers link...") + log.debug("Attempting to click the open offers link...") open_offers_link.click() try: # Now wait for the flyout to load @@ -629,7 +638,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): log.info("No offers found. Moving on.") return False log.info( - f"Found {len(offer_count)} offers in the HTML. Comparing offers..." + f"Found {len(offer_count)} offers for {asin}. Evaluating offers..." ) except sel_exceptions.TimeoutException as te: @@ -1428,6 +1437,7 @@ def show_config(self): log.info( f"Starting Amazon ASIN Hunt on {AMAZON_URLS['BASE_URL']} for {len(self.asin_list)} Products with:" ) + log.info(f"--Offer URL of: {self.ACTIVE_OFFER_URL}") log.info(f"--Delay of {self.refresh_delay} seconds") if self.headless: log.info(f"--Chrome is running in Headless mode") From dbfb0efecba8f12ad275cb9b11627eb05c8780ed Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Tue, 2 Feb 2021 17:34:37 -0500 Subject: [PATCH 055/150] Update amazon.py fixed headless prompt --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index 6c1595fc..b56d7ba4 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1436,7 +1436,7 @@ def show_config(self): log.info(f"--Notification sounds are disabled.") if self.headless: log.warning( - f"--Running headless is unsupported. If you get it to work, please let us know on Discord." + f"--Running in headless mode." ) if self.testing: log.warning(f"--Testing Mode. NO Purchases will be made.") From 2a47e33df6158ff31b630b03be9383a048d195e9 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 4 Feb 2021 12:18:34 -0500 Subject: [PATCH 056/150] -- Removed duplicate headless config option in show_config() --- stores/amazon.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index b56d7ba4..f800208a 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1434,10 +1434,6 @@ def show_config(self): log.info(f"--No images will be requested") if not self.notification_handler.sound_enabled: log.info(f"--Notification sounds are disabled.") - if self.headless: - log.warning( - f"--Running in headless mode." - ) if self.testing: log.warning(f"--Testing Mode. NO Purchases will be made.") log.info(f"{'=' * 50}") From 89e4b8ef2f3b18523cf15a1e713f4fb59b55ad86 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Thu, 4 Feb 2021 22:12:34 -0500 Subject: [PATCH 057/150] slowmode by default turned off page strategy none until logic is fixed --- stores/amazon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 628984f7..2017a717 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1503,8 +1503,8 @@ def create_driver(self, path_to_profile): prefs["profile.managed_default_content_settings.images"] = 0 options.add_experimental_option("prefs", prefs) options.add_argument(f"user-data-dir={path_to_profile}") - if not self.slow_mode: - options.set_capability("pageLoadStrategy", "none") + #if not self.slow_mode: + # options.set_capability("pageLoadStrategy", "none") self.setup_driver = False From ac2b71648601c3ba8fabd13c896671b9510f31cb Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 5 Feb 2021 09:41:56 -0500 Subject: [PATCH 058/150] Update from https://github.com/Hari-Nagarajan/fairgame/pull/484 --- config/fairgame.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/config/fairgame.conf b/config/fairgame.conf index 303b4eae..0911373e 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -47,6 +47,7 @@ "Cesta de compra Amazon.es", "Amazon.fr Panier", "Carrello Amazon.it", + "Pagamento Amazon.it", "AmazonSmile Shopping Cart", "AmazonSmile Shopping Basket", "Amazon.nl-winkelwagen" From 7387502f0036189327e2d131052d5de77af37d51 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 5 Feb 2021 10:19:27 -0500 Subject: [PATCH 059/150] Forced wait for page to fully load before determining all offers link availability. --- stores/amazon.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 2017a717..f2499f5d 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -525,20 +525,19 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): # Sanity check to see if we have any offers try: # Wait for the page to load before determining what's in it by looking for the footer - if self.slow_mode: - footer: WebElement = WebDriverWait( - self.driver, timeout=DEFAULT_MAX_TIMEOUT - ).until( - lambda d: d.find_elements_by_xpath( - "//div[@class='nav-footer-line'] | //img[@alt='Dogs of Amazon']" - ) + footer: WebElement = WebDriverWait( + self.driver, timeout=DEFAULT_MAX_TIMEOUT + ).until( + lambda d: d.find_elements_by_xpath( + "//div[@class='nav-footer-line'] | //img[@alt='Dogs of Amazon']" ) - if footer and footer[0].tag_name == "img": - log.info(f"Saw dogs for {asin}. Skipping...") - return False + ) + if footer and footer[0].tag_name == "img": + log.info(f"Saw dogs for {asin}. Skipping...") + return False - log.debug(f"After footer page title {self.driver.title}") - log.debug(f" page url: {self.driver.current_url}") + log.debug(f"After footer page title {self.driver.title}") + log.debug(f" page url: {self.driver.current_url}") offers = WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( lambda d: d.find_element_by_xpath( @@ -1503,8 +1502,8 @@ def create_driver(self, path_to_profile): prefs["profile.managed_default_content_settings.images"] = 0 options.add_experimental_option("prefs", prefs) options.add_argument(f"user-data-dir={path_to_profile}") - #if not self.slow_mode: - # options.set_capability("pageLoadStrategy", "none") + if not self.slow_mode: + options.set_capability("pageLoadStrategy", "none") self.setup_driver = False From 79bd6524d645c126918f1e867672b28d890c2cc2 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 5 Feb 2021 10:33:53 -0500 Subject: [PATCH 060/150] --Corrected help text on --alt-offers flag to represent defaulting to PDP --- cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cli.py b/cli/cli.py index 2ecb5791..8c0e1818 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -194,7 +194,7 @@ def main(): "--alt-offers", is_flag=True, default=False, - help="Directly hit the PDP for offers. Sub-optimal if you have the offers listing available to you.", + help="Directly hit the offers page. Preferred, but deprecated by Amazon.", ) @notify_on_crash def amazon( From e5695eacc9d0e75b65cf0d9a22c6be7a12d25a8e Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 5 Feb 2021 12:06:04 -0500 Subject: [PATCH 061/150] blackd --- app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app.py b/app.py index 9fa07dcd..8dc8ea53 100644 --- a/app.py +++ b/app.py @@ -30,6 +30,7 @@ def sha256sum(filename): h.update(mv[:n]) return h.hexdigest() + if os.path.exists("LICENSE") and sha256sum("LICENSE") in license_hash: s = """ FairGame Copyright (C) 2021 Hari Nagarajan From ce3929dc9bcdc8de585a358803adabfd4c223e27 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 5 Feb 2021 12:19:22 -0500 Subject: [PATCH 062/150] --Updated version to account for development and find-offers co-existing as dev3. --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 8418fd81..33a61291 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev3" +__VERSION = "0.6.0.dev5" version = Version(__VERSION) From 85d4575d499d740b6224cf34b329890ddcd6ab60 Mon Sep 17 00:00:00 2001 From: Cole Gerdemann Date: Mon, 8 Feb 2021 11:37:00 -0500 Subject: [PATCH 063/150] edit captcha xpath to be more universal checks for the actual captcha url in the form vs an element whose id may change --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index f2499f5d..f7fa755c 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -398,7 +398,7 @@ def login(self): # check for captcha try: captcha_entry = self.driver.find_element_by_xpath( - '//*[@id="auth-captcha-guess"]' + '//*[@action="https://www.amazon.com/errors/validateCaptcha"]' ) except sel_exceptions.NoSuchElementException: password_field.send_keys(Keys.RETURN) From e04be3827b2c60c4fb042ee262a2755b47b13a20 Mon Sep 17 00:00:00 2001 From: Cole Gerdemann Date: Mon, 8 Feb 2021 11:42:11 -0500 Subject: [PATCH 064/150] not dependent on amazon.com domain --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index f7fa755c..933fdd1d 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -398,7 +398,7 @@ def login(self): # check for captcha try: captcha_entry = self.driver.find_element_by_xpath( - '//*[@action="https://www.amazon.com/errors/validateCaptcha"]' + '//form[contains(@action,"validateCaptcha")]' ) except sel_exceptions.NoSuchElementException: password_field.send_keys(Keys.RETURN) From 1d0089d6344b30888d4820aa938ab02bc596d46e Mon Sep 17 00:00:00 2001 From: Cole Gerdemann Date: Mon, 8 Feb 2021 13:12:00 -0500 Subject: [PATCH 065/150] merge duplicate captcha solving code --- stores/amazon.py | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 933fdd1d..875cce84 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -407,29 +407,7 @@ def login(self): log.error("Password entry box did not exist") if captcha_entry: - try: - log.info("Stuck on a captcha... Lets try to solve it.") - captcha = AmazonCaptcha.fromdriver(self.driver) - solution = captcha.solve() - log.info(f"The solution is: {solution}") - if solution == "Not solved": - log.info( - f"Failed to solve {captcha.image_link}, lets reload and get a new captcha." - ) - self.driver.refresh() - else: - self.send_notification( - "Solving catpcha", "captcha", self.take_screenshots - ) - captcha_entry.send_keys(solution + Keys.RETURN) - self.wait_for_page_change(current_page) - - except Exception as e: - log.debug(e) - log.info("Error trying to solve captcha. Refresh and retry.") - self.driver.refresh() - time.sleep(5) - + self.handle_captcha(False) if self.driver.title in amazon_config["TWOFA_TITLES"]: log.info("enter in your two-step verification code in browser") while self.driver.title in amazon_config["TWOFA_TITLES"]: @@ -478,6 +456,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): fail_counter = 0 presence.searching_update() + # handles initial page load only while True: try: self.get_page(f.url) @@ -536,6 +515,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): log.info(f"Saw dogs for {asin}. Skipping...") return False + log.debug(f"After footer page title {self.driver.title}") log.debug(f" page url: {self.driver.current_url}") @@ -1280,13 +1260,13 @@ def handle_out_of_stock(self): self.try_to_checkout = False @debug - def handle_captcha(self): + def handle_captcha(self, check_presence=True): # wait for captcha to load time.sleep(DEFAULT_MAX_WEIRD_PAGE_DELAY) current_page = self.driver.title try: - if self.driver.find_element_by_xpath( - '//form[@action="/errors/validateCaptcha"]' + if not check_presence or self.driver.find_element_by_xpath( + '//form[contains(@action,"validateCaptcha")]' ): try: log.info("Stuck on a captcha... Lets try to solve it.") From 0111c17b2f2a9f9af781961db344cc971dff42f4 Mon Sep 17 00:00:00 2001 From: Cole Gerdemann Date: Mon, 8 Feb 2021 13:50:41 -0500 Subject: [PATCH 066/150] handle captcha appearing during check_stock and added captcha title "Server Busy" to fairgame.conf --- config/fairgame.conf | 5 +++-- stores/amazon.py | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 0911373e..2d07ca39 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -22,7 +22,8 @@ "Inloggen bij Amazon" ], "CAPTCHA_PAGE_TITLES": [ - "Robot Check" + "Robot Check", + "Server Busy" ], "HOME_PAGE_TITLES": [ "Amazon.com: Online Shopping for Electronics, Apparel, Computers, Books, DVDs & more", @@ -130,4 +131,4 @@ "Select a shipping address" ] } -} \ No newline at end of file +} diff --git a/stores/amazon.py b/stores/amazon.py index 875cce84..96d511d6 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -462,6 +462,8 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): self.get_page(f.url) log.debug(f"Initial page title {self.driver.title}") log.debug(f" page url: {self.driver.current_url}") + if self.driver.title in amazon_config["CAPTCHA_PAGE_TITLES"]: + self.handle_captcha() break except Exception: fail_counter += 1 @@ -515,7 +517,6 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): log.info(f"Saw dogs for {asin}. Skipping...") return False - log.debug(f"After footer page title {self.driver.title}") log.debug(f" page url: {self.driver.current_url}") @@ -1262,6 +1263,7 @@ def handle_out_of_stock(self): @debug def handle_captcha(self, check_presence=True): # wait for captcha to load + log.debug("Waiting for captcha to load.") time.sleep(DEFAULT_MAX_WEIRD_PAGE_DELAY) current_page = self.driver.title try: From ea6ae0474d93c4313217b59a9f5051863b23eb6a Mon Sep 17 00:00:00 2001 From: Crasoum <44730377+Crasoum@users.noreply.github.com> Date: Wed, 10 Feb 2021 08:27:47 -0600 Subject: [PATCH 067/150] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 807b28a8..d43668f2 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ click. !!! YOU WILL NEED TO USE THE 3.8 BRANCH OF PYTHON, 3.9.0 BREAKS DEPENDENCIES !!! +It is best if you use the newest version (3.8.7) but 3.8.5 and 3.8.6 should also work. 3.8.0 does not. + ```shell pip install pipenv pipenv shell From ac00e72bc2533d4a917ee76920d75268063ce7bc Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 10 Feb 2021 09:54:11 -0500 Subject: [PATCH 068/150] --added emtpy cart check --- stores/amazon.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index 96d511d6..c72b32ab 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -799,10 +799,16 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False self.wait_for_page_change(current_title) # log.info(f"page title is {self.driver.title}") - if self.driver.title in amazon_config["SHOPPING_CART_TITLES"]: + emtpy_cart_elements = self.driver.find_elements_by_xpath( + "//div[contains(@class, 'sc-your-amazon-cart-is-empty') or contains(@class, 'sc-empty-cart')]" + ) + + if not emtpy_cart_elements and self.driver.title in amazon_config["SHOPPING_CART_TITLES"]: return True else: log.info("did not add to cart, trying again") + if emtpy_cart_elements: + log.info("Cart appeared empty after clicking Add To Cart button") log.debug(f"failed title was {self.driver.title}") self.send_notification( "Failed Add to Cart", "failed-atc", self.take_screenshots From 0f0254cfb8479642671399723136d374b953b152 Mon Sep 17 00:00:00 2001 From: Crasoum <44730377+Crasoum@users.noreply.github.com> Date: Wed, 10 Feb 2021 11:05:27 -0600 Subject: [PATCH 069/150] Update readme with new cheat-sheet Update readme with new cheat-sheet --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d43668f2..4c388c13 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ if that does not answer your questions. Community user Easy_XII has created a great cheat sheet for getting started. It includes specific and additional steps for Windows users as well as useful product and configuration information. Please start -with [this guide](https://docs.google.com/document/d/1grN282tPodM9N57bPq4bbNyKZC01t_4A-sLpzzu_7lM/) to get you started +with [this guide](https://docs.google.com/document/d/14kZ0SNC97DFVRStnrdsJ8xbQO1m42v7svy93kUdtX48) to get you started and to answer any initial questions you may have about setup. **Note:** The above document is community maintained and managed. The authors of Fairgame do not control the contents, From ee487c1fafe87ebae74de8405feef1a5a96440c8 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 10 Feb 2021 15:55:25 -0500 Subject: [PATCH 070/150] --Initial implementation of offerID add to cart mechanics inspired by Dakk's proto-type method --- stores/amazon.py | 147 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 111 insertions(+), 36 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index c72b32ab..82d826b7 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -23,6 +23,7 @@ import os import platform import time +from contextlib import contextmanager from datetime import datetime from enum import Enum @@ -35,8 +36,10 @@ from pypresence import exceptions as pyexceptions from selenium import webdriver from selenium.common import exceptions as sel_exceptions +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait from utils import discord_presence as presence @@ -781,47 +784,97 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ): log.info("Item in stock and in reserve range!") log.info("clicking add to cart") - self.notification_handler.play_notify_sound() - if self.detailed: - self.send_notification( - message=f"Found Stock ASIN:{asin}", - page_name="Stock Alert", - take_screenshot=self.take_screenshots, - ) - - presence.buy_update() - current_title = self.driver.title - # log.info(f"current page title is {current_title}") - try: - atc_button.click() - except IndexError: - log.debug("Index Error") - return False - self.wait_for_page_change(current_title) - # log.info(f"page title is {self.driver.title}") - emtpy_cart_elements = self.driver.find_elements_by_xpath( - "//div[contains(@class, 'sc-your-amazon-cart-is-empty') or contains(@class, 'sc-empty-cart')]" + # Get the offering ID + offering_id_elements = atc_button.find_elements_by_xpath( + "./preceding::input[@name='offeringID.1'][1]" ) - - if not emtpy_cart_elements and self.driver.title in amazon_config["SHOPPING_CART_TITLES"]: - return True + if offering_id_elements: + log.info("Attempting Add To Cart with offer ID...") + offering_id = offering_id_elements[0].get_attribute("value") + if self.attempt_atc(offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): + return True + else: + self.send_notification( + "Failed Add to Cart after {max-atc-retries}", + "failed-atc", + self.take_screenshots, + ) + self.save_page_source("failed-atc") + return False else: - log.info("did not add to cart, trying again") - if emtpy_cart_elements: - log.info("Cart appeared empty after clicking Add To Cart button") - log.debug(f"failed title was {self.driver.title}") - self.send_notification( - "Failed Add to Cart", "failed-atc", self.take_screenshots + log.error( + "Unable to find offering ID to add to cart. Using legacy mode." ) - self.save_page_source("failed-atc") - in_stock = self.check_stock( - asin=asin, - reserve_max=reserve_max, - reserve_min=reserve_min, - retry=retry + 1, + self.notification_handler.play_notify_sound() + if self.detailed: + self.send_notification( + message=f"Found Stock ASIN:{asin}", + page_name="Stock Alert", + take_screenshot=self.take_screenshots, + ) + + presence.buy_update() + current_title = self.driver.title + # log.info(f"current page title is {current_title}") + try: + atc_button.click() + except IndexError: + log.debug("Index Error") + return False + self.wait_for_page_change(current_title) + # log.info(f"page title is {self.driver.title}") + emtpy_cart_elements = self.driver.find_elements_by_xpath( + "//div[contains(@class, 'sc-your-amazon-cart-is-empty') or contains(@class, 'sc-empty-cart')]" ) + + if ( + not emtpy_cart_elements + and self.driver.title in amazon_config["SHOPPING_CART_TITLES"] + ): + return True + else: + log.info("did not add to cart, trying again") + if emtpy_cart_elements: + log.info( + "Cart appeared empty after clicking Add To Cart button" + ) + log.debug(f"failed title was {self.driver.title}") + self.send_notification( + "Failed Add to Cart", "failed-atc", self.take_screenshots + ) + self.save_page_source("failed-atc") + in_stock = self.check_stock( + asin=asin, + reserve_max=reserve_max, + reserve_min=reserve_min, + retry=retry + 1, + ) return in_stock + def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): + # Open the add.html URL in Selenium + f = f"https://smile.amazon.com/gp/aws/cart/add.html?OfferListingId.1={offering_id}&Quantity.1=1" + atc_attempts = 0 + while atc_attempts < max_atc_retries: + with self.wait_for_page_content_change(timeout=5): + self.driver.get(f) + xpath = "//input[@alt='Continue']" + if wait_for_element_by_xpath(self.driver, xpath): + try: + with self.wait_for_page_content_change(timeout=10): + self.driver.find_element_by_xpath(xpath).click() + except sel_exceptions.NoSuchElementException: + log.error("Continue button not present on page") + else: + log.error("Continue button not present on page") + + # verify cart is non-zero + if self.get_cart_count() != 0: + return True + else: + atc_attempts = atc_attempts + 1 + return False + # search lists of asin lists, and remove the first list that matches provided asin @debug def remove_asin_list(self, asin): @@ -1152,7 +1205,7 @@ def handle_cart(self): while True: try: button = self.driver.find_element_by_xpath( - '//*[@id="hlb-ptc-btn-native"]' + '//*[@id="hlb-ptc-btn-native"] | //input[@name="proceedToRetailCheckout"]' ) break except sel_exceptions.NoSuchElementException: @@ -1355,6 +1408,16 @@ def save_page_source(self, page): with open(file_name, "w", encoding="utf-8") as f: f.write(page_source) + @contextmanager + def wait_for_page_content_change(self, timeout=30): + """Utility to help manage selenium waiting for a page to load after an action, like a click""" + old_page = self.driver.find_element_by_tag_name("html") + yield + WebDriverWait(self.driver, timeout).until(EC.staleness_of(old_page)) + WebDriverWait(self.driver, timeout).until( + EC.presence_of_element_located((By.XPATH, "//title")) + ) + def wait_for_page_change(self, page_title, timeout=3): time_to_end = self.get_timeout(timeout=timeout) while time.time() < time_to_end and ( @@ -1687,3 +1750,15 @@ def get_item_condition(form_action) -> AmazonItemCondition: else: # log.debug(f"Item condition is unknown: {form_action}") return AmazonItemCondition.Unknown + + +def wait_for_element_by_xpath(d, xpath, timeout=10): + try: + WebDriverWait(d, timeout).until( + EC.presence_of_element_located((By.XPATH, xpath)) + ) + except sel_exceptions.TimeoutException: + log.error(f"failed to find {xpath}") + return False + + return True From 0b315bbd08fb1f2c77ec08dd1d2445431fad83de Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 10 Feb 2021 17:46:19 -0500 Subject: [PATCH 071/150] --Redirect user to the cart page if they have existing items before exiting --- stores/amazon.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 82d826b7..14412577 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -221,6 +221,7 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): if cart_quantity > 0: log.warning(f"Found {cart_quantity} item(s) in your cart.") log.info("Delete all item(s) in cart before starting bot.") + self.driver.get(AMAZON_URLS["CART_URL"]) log.info("Exiting in 30 seconds...") time.sleep(30) return @@ -234,8 +235,9 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): if self.get_cart_count() > 0: log.warning(f"Found {cart_quantity} item(s) in your cart.") log.info("Delete all item(s) in cart before starting bot.") - log.info("Exiting now...") - time.sleep(5) + self.driver.get(AMAZON_URLS["CART_URL"]) + log.info("Exiting in 30 seconds...") + time.sleep(30) return keep_going = True From 160a93ab520afb374feddd063149ea69bec421da Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 10 Feb 2021 17:46:39 -0500 Subject: [PATCH 072/150] --Incremented version for tracking purposes --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 33a61291..733d2f39 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev5" +__VERSION = "0.6.0.dev6" version = Version(__VERSION) From 3416430199b1ed28dbb20bc99e00053b97d54561 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 11 Feb 2021 08:11:12 -0500 Subject: [PATCH 073/150] --Updated Cheatsheet link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4167428e..8ee68def 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Read through this document and the cheat sheet linked in the next sections. See ## Installation -Easy_XII has created a great cheat sheet for getting started, [please follow this guide](https://docs.google.com/document/d/1grN282tPodM9N57bPq4bbNyKZC01t_4A-sLpzzu_7lM/). +Easy_XII has created a great cheat sheet for getting started, [please follow this guide](https://docs.google.com/document/d/14kZ0SNC97DFVRStnrdsJ8xbQO1m42v7svy93kUdtX48/). **Note:** that we do not control the contents of this document, so use some common sense when configuring the bot. Do not ask us why the bot does not purchase an $8.49 item when the minimum purchase price is set to $10 in the configuration file that YOU are supposed to update From 1efbe98ae2b4ce41f0ac94ddb6ea19337cb6dfb3 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 11 Feb 2021 08:51:35 -0500 Subject: [PATCH 074/150] --Hotfix for missing place order button selector (new selector?) --- stores/amazon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stores/amazon.py b/stores/amazon.py index 96d511d6..15c033b4 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -56,6 +56,7 @@ AUTOBUY_CONFIG_PATH = "config/amazon_config.json" BUTTON_XPATHS = [ + '//input[@name="placeYourOrder1"]', '//*[@id="submitOrderButtonId"]/span/input', '//*[@id="bottomSubmitOrderButtonId"]/span/input', '//*[@id="placeYourOrder"]/span/input', From aded08037eef434de0376f2564dd265dfed11dcf Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 10 Feb 2021 09:54:11 -0500 Subject: [PATCH 075/150] --added emtpy cart check --- stores/amazon.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index 15c033b4..bb5f3043 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -800,10 +800,16 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False self.wait_for_page_change(current_title) # log.info(f"page title is {self.driver.title}") - if self.driver.title in amazon_config["SHOPPING_CART_TITLES"]: + emtpy_cart_elements = self.driver.find_elements_by_xpath( + "//div[contains(@class, 'sc-your-amazon-cart-is-empty') or contains(@class, 'sc-empty-cart')]" + ) + + if not emtpy_cart_elements and self.driver.title in amazon_config["SHOPPING_CART_TITLES"]: return True else: log.info("did not add to cart, trying again") + if emtpy_cart_elements: + log.info("Cart appeared empty after clicking Add To Cart button") log.debug(f"failed title was {self.driver.title}") self.send_notification( "Failed Add to Cart", "failed-atc", self.take_screenshots From bb93536136733a95754f3f80226d23124f450632 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 10 Feb 2021 15:55:25 -0500 Subject: [PATCH 076/150] --Initial implementation of offerID add to cart mechanics inspired by Dakk's proto-type method --- stores/amazon.py | 147 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 111 insertions(+), 36 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index bb5f3043..8d7a4e18 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -23,6 +23,7 @@ import os import platform import time +from contextlib import contextmanager from datetime import datetime from enum import Enum @@ -35,8 +36,10 @@ from pypresence import exceptions as pyexceptions from selenium import webdriver from selenium.common import exceptions as sel_exceptions +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait from utils import discord_presence as presence @@ -782,47 +785,97 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ): log.info("Item in stock and in reserve range!") log.info("clicking add to cart") - self.notification_handler.play_notify_sound() - if self.detailed: - self.send_notification( - message=f"Found Stock ASIN:{asin}", - page_name="Stock Alert", - take_screenshot=self.take_screenshots, - ) - - presence.buy_update() - current_title = self.driver.title - # log.info(f"current page title is {current_title}") - try: - atc_button.click() - except IndexError: - log.debug("Index Error") - return False - self.wait_for_page_change(current_title) - # log.info(f"page title is {self.driver.title}") - emtpy_cart_elements = self.driver.find_elements_by_xpath( - "//div[contains(@class, 'sc-your-amazon-cart-is-empty') or contains(@class, 'sc-empty-cart')]" + # Get the offering ID + offering_id_elements = atc_button.find_elements_by_xpath( + "./preceding::input[@name='offeringID.1'][1]" ) - - if not emtpy_cart_elements and self.driver.title in amazon_config["SHOPPING_CART_TITLES"]: - return True + if offering_id_elements: + log.info("Attempting Add To Cart with offer ID...") + offering_id = offering_id_elements[0].get_attribute("value") + if self.attempt_atc(offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): + return True + else: + self.send_notification( + "Failed Add to Cart after {max-atc-retries}", + "failed-atc", + self.take_screenshots, + ) + self.save_page_source("failed-atc") + return False else: - log.info("did not add to cart, trying again") - if emtpy_cart_elements: - log.info("Cart appeared empty after clicking Add To Cart button") - log.debug(f"failed title was {self.driver.title}") - self.send_notification( - "Failed Add to Cart", "failed-atc", self.take_screenshots + log.error( + "Unable to find offering ID to add to cart. Using legacy mode." ) - self.save_page_source("failed-atc") - in_stock = self.check_stock( - asin=asin, - reserve_max=reserve_max, - reserve_min=reserve_min, - retry=retry + 1, + self.notification_handler.play_notify_sound() + if self.detailed: + self.send_notification( + message=f"Found Stock ASIN:{asin}", + page_name="Stock Alert", + take_screenshot=self.take_screenshots, + ) + + presence.buy_update() + current_title = self.driver.title + # log.info(f"current page title is {current_title}") + try: + atc_button.click() + except IndexError: + log.debug("Index Error") + return False + self.wait_for_page_change(current_title) + # log.info(f"page title is {self.driver.title}") + emtpy_cart_elements = self.driver.find_elements_by_xpath( + "//div[contains(@class, 'sc-your-amazon-cart-is-empty') or contains(@class, 'sc-empty-cart')]" ) + + if ( + not emtpy_cart_elements + and self.driver.title in amazon_config["SHOPPING_CART_TITLES"] + ): + return True + else: + log.info("did not add to cart, trying again") + if emtpy_cart_elements: + log.info( + "Cart appeared empty after clicking Add To Cart button" + ) + log.debug(f"failed title was {self.driver.title}") + self.send_notification( + "Failed Add to Cart", "failed-atc", self.take_screenshots + ) + self.save_page_source("failed-atc") + in_stock = self.check_stock( + asin=asin, + reserve_max=reserve_max, + reserve_min=reserve_min, + retry=retry + 1, + ) return in_stock + def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): + # Open the add.html URL in Selenium + f = f"https://smile.amazon.com/gp/aws/cart/add.html?OfferListingId.1={offering_id}&Quantity.1=1" + atc_attempts = 0 + while atc_attempts < max_atc_retries: + with self.wait_for_page_content_change(timeout=5): + self.driver.get(f) + xpath = "//input[@alt='Continue']" + if wait_for_element_by_xpath(self.driver, xpath): + try: + with self.wait_for_page_content_change(timeout=10): + self.driver.find_element_by_xpath(xpath).click() + except sel_exceptions.NoSuchElementException: + log.error("Continue button not present on page") + else: + log.error("Continue button not present on page") + + # verify cart is non-zero + if self.get_cart_count() != 0: + return True + else: + atc_attempts = atc_attempts + 1 + return False + # search lists of asin lists, and remove the first list that matches provided asin @debug def remove_asin_list(self, asin): @@ -1153,7 +1206,7 @@ def handle_cart(self): while True: try: button = self.driver.find_element_by_xpath( - '//*[@id="hlb-ptc-btn-native"]' + '//*[@id="hlb-ptc-btn-native"] | //input[@name="proceedToRetailCheckout"]' ) break except sel_exceptions.NoSuchElementException: @@ -1356,6 +1409,16 @@ def save_page_source(self, page): with open(file_name, "w", encoding="utf-8") as f: f.write(page_source) + @contextmanager + def wait_for_page_content_change(self, timeout=30): + """Utility to help manage selenium waiting for a page to load after an action, like a click""" + old_page = self.driver.find_element_by_tag_name("html") + yield + WebDriverWait(self.driver, timeout).until(EC.staleness_of(old_page)) + WebDriverWait(self.driver, timeout).until( + EC.presence_of_element_located((By.XPATH, "//title")) + ) + def wait_for_page_change(self, page_title, timeout=3): time_to_end = self.get_timeout(timeout=timeout) while time.time() < time_to_end and ( @@ -1688,3 +1751,15 @@ def get_item_condition(form_action) -> AmazonItemCondition: else: # log.debug(f"Item condition is unknown: {form_action}") return AmazonItemCondition.Unknown + + +def wait_for_element_by_xpath(d, xpath, timeout=10): + try: + WebDriverWait(d, timeout).until( + EC.presence_of_element_located((By.XPATH, xpath)) + ) + except sel_exceptions.TimeoutException: + log.error(f"failed to find {xpath}") + return False + + return True From c848de3c26ae7cf33316a766d26c9c2f0412b333 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 10 Feb 2021 17:46:19 -0500 Subject: [PATCH 077/150] --Redirect user to the cart page if they have existing items before exiting --- stores/amazon.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 8d7a4e18..e470fe66 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -222,6 +222,7 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): if cart_quantity > 0: log.warning(f"Found {cart_quantity} item(s) in your cart.") log.info("Delete all item(s) in cart before starting bot.") + self.driver.get(AMAZON_URLS["CART_URL"]) log.info("Exiting in 30 seconds...") time.sleep(30) return @@ -235,8 +236,9 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): if self.get_cart_count() > 0: log.warning(f"Found {cart_quantity} item(s) in your cart.") log.info("Delete all item(s) in cart before starting bot.") - log.info("Exiting now...") - time.sleep(5) + self.driver.get(AMAZON_URLS["CART_URL"]) + log.info("Exiting in 30 seconds...") + time.sleep(30) return keep_going = True From bd0bd86787c2cef82b9e93fa87d2efb51ac7e792 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 10 Feb 2021 17:46:39 -0500 Subject: [PATCH 078/150] --Incremented version for tracking purposes --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 33a61291..733d2f39 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev5" +__VERSION = "0.6.0.dev6" version = Version(__VERSION) From c8a56fbd1c7c9d741ab52f9287a48fb4adbf67a0 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 11 Feb 2021 09:25:39 -0500 Subject: [PATCH 079/150] --Added multi-domain support for ATC url --- stores/amazon.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index e470fe66..038dfab3 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -53,6 +53,7 @@ "ALT_OFFER_URL": "https://{domain}/gp/offer-listing/", "OFFER_URL": "https://{domain}/dp/", "CART_URL": "https://{domain}/gp/cart/view.html", + "ATC_URL": "https://{domain}/gp/aws/cart/add.html", } CHECKOUT_URL = "https://{domain}/gp/cart/desktop/go-to-checkout.html/ref=ox_sc_proceed?partialCheckoutCart=1&isToBeGiftWrappedBefore=0&proceedToRetailCheckout=Proceed+to+checkout&proceedToCheckout=1&cartInitiateId={cart_id}" @@ -794,7 +795,9 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): if offering_id_elements: log.info("Attempting Add To Cart with offer ID...") offering_id = offering_id_elements[0].get_attribute("value") - if self.attempt_atc(offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): + if self.attempt_atc( + offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES + ): return True else: self.send_notification( @@ -856,7 +859,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): # Open the add.html URL in Selenium - f = f"https://smile.amazon.com/gp/aws/cart/add.html?OfferListingId.1={offering_id}&Quantity.1=1" + f = f"{AMAZON_URLS['ATC_URL']}?OfferListingId.1={offering_id}&Quantity.1=1" atc_attempts = 0 while atc_attempts < max_atc_retries: with self.wait_for_page_content_change(timeout=5): From 824fa8aa4eb3b763fd75f80d077cfb6b39f87e9e Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 13 Feb 2021 08:47:23 -0500 Subject: [PATCH 080/150] --Updated version to 0.6.0.dev7 --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 733d2f39..394464f7 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev6" +__VERSION = "0.6.0.dev7" version = Version(__VERSION) From 94692d2c7001608918e9fef4c4e69d8e173a1897 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 13 Feb 2021 11:09:06 -0500 Subject: [PATCH 081/150] --Updated "Continue" selector to be non-English compatible --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index 038dfab3..c8fb7b23 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -864,7 +864,7 @@ def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): while atc_attempts < max_atc_retries: with self.wait_for_page_content_change(timeout=5): self.driver.get(f) - xpath = "//input[@alt='Continue']" + xpath = "//input[@value='add' and @name='add']" if wait_for_element_by_xpath(self.driver, xpath): try: with self.wait_for_page_content_change(timeout=10): From 6dada5fd564fa3fb2e5f58963a584b29e4292676 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 13 Feb 2021 11:09:38 -0500 Subject: [PATCH 082/150] --Updated version to 0.6.0.dev8 --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 394464f7..654a4e68 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev7" +__VERSION = "0.6.0.dev8" version = Version(__VERSION) From 0d3840503aaf535220874cc9b28d38a9027d5428 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 13 Feb 2021 15:32:51 -0500 Subject: [PATCH 083/150] --Additional logging for supporting users --Corrected shopping cart title for Italy --- config/fairgame.conf | 4 ++-- stores/amazon.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 2d07ca39..6bfd9af1 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -48,7 +48,6 @@ "Cesta de compra Amazon.es", "Amazon.fr Panier", "Carrello Amazon.it", - "Pagamento Amazon.it", "AmazonSmile Shopping Cart", "AmazonSmile Shopping Basket", "Amazon.nl-winkelwagen" @@ -74,7 +73,8 @@ "Place Your Order - AmazonSmile Checkout", "Preparing your order", "Ihre Bestellung wird vorbereitet", - "Pagamento Amazon.it" + "Pagamento Amazon.it", + "Ordine in preparazione" ], "ORDER_COMPLETE_TITLES": [ "Amazon.com Thanks You", diff --git a/stores/amazon.py b/stores/amazon.py index c8fb7b23..32626e06 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -787,7 +787,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): or math.isclose((price_float + ship_float), reserve_min, abs_tol=0.01) ): log.info("Item in stock and in reserve range!") - log.info("clicking add to cart") + log.info("Adding to cart") # Get the offering ID offering_id_elements = atc_button.find_elements_by_xpath( "./preceding::input[@name='offeringID.1'][1]" @@ -898,6 +898,7 @@ def navigate_pages(self, test): # time.sleep(self.page_wait_delay()) title = self.driver.title + log.info(f"Navigating page title: '{title}'") # see if this resolves blank page title issue? if title == "": timeout_seconds = DEFAULT_MAX_TIMEOUT @@ -1201,7 +1202,7 @@ def handle_home_page(self): @debug def handle_cart(self): self.start_time_atc = time.time() - log.info("clicking checkout.") + log.info("Looking for Proceed To Checkout button...") try: self.save_screenshot("ptc-page") except: @@ -1243,17 +1244,17 @@ def handle_cart(self): current_page = self.driver.title if button: + log.info("Found Checkout Button") if self.detailed: self.send_notification( message="Attempting to Proceed to Checkout", page_name="ptc", take_screenshot=self.take_screenshots, ) - log.info("Found Checkout Button") try: button.click() log.info("Clicked Proceed to Checkout Button") - self.wait_for_page_change(page_title=current_page) + self.wait_for_page_change(page_title=current_page, timeout=7) except sel_exceptions.WebDriverException: log.error("Problem clicking Proceed to Checkout button.") log.info("Refreshing page to try again") From b45bdeaf5a2273c1aa833597e216553a241ffdee Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 13 Feb 2021 15:33:20 -0500 Subject: [PATCH 084/150] --Updated to version 0.6.0.dev9 --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 654a4e68..44ad8f0a 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev8" +__VERSION = "0.6.0.dev9" version = Version(__VERSION) From 94b2482a07bd7aa41ab6e7d8817d78a21ca9ce9f Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sun, 14 Feb 2021 11:44:46 -0500 Subject: [PATCH 085/150] --Added support for empty shipping div that was causing a crash --Added support for non-Prime member price parsing for shipping. Untested. --- stores/amazon.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 32626e06..29371d0c 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -32,7 +32,7 @@ from chromedriver_py import binary_path # this will get you the path variable from furl import furl from lxml import html -from price_parser import parse_price +from price_parser import parse_price, Price from pypresence import exceptions as pyexceptions from selenium import webdriver from selenium.common import exceptions as sel_exceptions @@ -1630,7 +1630,7 @@ def get_timestamp_filename(name, extension): return name + "_" + date + "." + extension -def get_shipping_costs(tree, free_shipping_string): +def get_shipping_costs(tree, free_shipping_string) -> Price: # Assume Free Shipping and change otherwise # Shipping collection xpath: @@ -1650,8 +1650,32 @@ def get_shipping_costs(tree, free_shipping_string): # Shipping information is found within either a DIV or a SPAN following the bottleDepositFee DIV # What follows is logic to parse out the various pricing formats within the HTML. Not ideal, but # it's what we have to work with. - shipping_span_text = shipping_node.text.strip() + if shipping_node.text: + shipping_span_text = shipping_node.text.strip() + else: + shipping_span_text = "" if shipping_node.tag == "div": + # Do we have any spans outlining the price? Typically seen like this: + #
+ # + + # S$21.44 + # shipping + #
+ shipping_spans = shipping_node.xpath(".//span") + if shipping_spans: + log.debug( + f"Found {len(shipping_spans)} shipping SPANs within the shipping DIV" + ) + # Look for a price + for shipping_span in shipping_spans: + if shipping_span.text and shipping_span.text != "+": + shipping_cost: Price = parse_price(shipping_span.text) + if shipping_cost.currency is not None: + log.debug( + f"Found parseable price with currency symbol: {shipping_cost.currency}" + ) + return shipping_cost + if shipping_span_text == "": # Assume zero shipping for an empty div log.debug( From 19be0272f726a279259724f99d78899e52d9725f Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sun, 14 Feb 2021 11:48:56 -0500 Subject: [PATCH 086/150] --Updated version to 0.6.0.dev10 --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 44ad8f0a..b2ba129e 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev9" +__VERSION = "0.6.0.dev10" version = Version(__VERSION) From 1f1ef2518fa92ad5fd62cf368f22bb9ec324b586 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Mon, 15 Feb 2021 18:11:23 -0500 Subject: [PATCH 087/150] Update version.py updated version number --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index b2ba129e..d275ce97 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0.dev10" +__VERSION = "0.6.0" version = Version(__VERSION) From 7ee5bbf60e66901098ac17ff17baaa79e8aa6ce4 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 19 Feb 2021 13:18:05 -0500 Subject: [PATCH 088/150] --Added support for lower cost encryption for Pi devices (blindly downgrading for any system reporting as "arm") --- utils/encryption.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/utils/encryption.py b/utils/encryption.py index 92516cda..1e729f35 100644 --- a/utils/encryption.py +++ b/utils/encryption.py @@ -17,23 +17,31 @@ # The author may be contacted through the project's GitHub, at: # https://github.com/Hari-Nagarajan/fairgame -import getpass as getpass -import stdiomask import json -import os +import platform from base64 import b64encode, b64decode + +import stdiomask from Crypto.Cipher import ChaCha20_Poly1305 -from Crypto.Random import get_random_bytes from Crypto.Protocol.KDF import scrypt +from Crypto.Random import get_random_bytes from utils.logger import log +if platform.machine()[:3] == "arm": + # Not great, but workable way to detect Raspberry Pi + log.info("Using reduced CPU and Memory cost parameter for encryption for your platform.") + CPU_MEM_COST = 10 +else: + CPU_MEM_COST = 20 + def encrypt(pt, password): """Encryption function to securely store user credentials, uses ChaCha_Poly1305 with a user defined SCrypt key.""" salt = get_random_bytes(32) - key = scrypt(password, salt, key_len=32, N=2 ** 20, r=8, p=1) + + key = scrypt(password, salt, key_len=32, N=2 ** CPU_MEM_COST, r=8, p=1) nonce = get_random_bytes(12) cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) ct, tag = cipher.encrypt_and_digest(pt) @@ -51,7 +59,9 @@ def decrypt(ct, password): json_k = ["nonce", "salt", "ct", "tag"] json_v = {k: b64decode(b64Ct[k]) for k in json_k} - key = scrypt(password, json_v["salt"], key_len=32, N=2 ** 20, r=8, p=1) + key = scrypt( + password, json_v["salt"], key_len=32, N=2 ** CPU_MEM_COST, r=8, p=1 + ) cipher = ChaCha20_Poly1305.new(key=key, nonce=json_v["nonce"]) ptData = cipher.decrypt_and_verify(json_v["ct"], json_v["tag"]) From 75e6aaa735a090a7908624e2aba5b66f671d9a80 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 19 Feb 2021 13:22:48 -0500 Subject: [PATCH 089/150] --added basic name resolution evaluator --added traceroute generator for resolved IPs for a given domain --- cli/cli.py | 78 ++++++++++++++++++++++++++++++++++++++++-- common/globalconfig.py | 8 +++++ config/fairgame.conf | 32 ++++++++++++++++- 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/cli/cli.py b/cli/cli.py index 8c0e1818..bbba1cd4 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -19,17 +19,18 @@ import os import shutil +import platform from datetime import datetime from functools import wraps from pathlib import Path from signal import signal, SIGINT +from icmplib import ping, multiping, traceroute, resolve, Host, Hop LICENSE_PATH = os.path.join( "cli", "license", ) - try: import click except ModuleNotFoundError as e: @@ -38,7 +39,6 @@ exit(0) import time - from notifications.notifications import NotificationHandler, TIME_FORMAT from utils.logger import log from common.globalconfig import GlobalConfig, AMAZON_CREDENTIAL_FILE @@ -80,7 +80,6 @@ def decorator(*args, **kwargs): @click.group() def main(): - pass @@ -328,12 +327,85 @@ def show(w, c): exit(0) +@click.command() +@click.option("--domain", help="Specify the domain you want to find endpoints for.") +def find_endpoints(domain): + import dns.resolver + + if not domain: + log.error("You must specify a domain to resolve for endpoints with --domain.") + exit(0) + # Default + my_resolver = dns.resolver.Resolver() + resolved = my_resolver.resolve(domain) + for rdata in resolved: + log.info(f"Your computer resolves {domain} to {rdata.address}") + + # Find endpoints from various DNS servers + endpoints, resolutions = resolve_domain(domain) + log.info( + f"{domain} resolves to at least {len(endpoints)} distinct IP addresses across {resolutions} lookups:" + ) + endpoints = sorted(endpoints) + for endpoint in endpoints: + log.info(f" {endpoint}") + + return endpoints + + +def resolve_domain(domain): + import dns.resolver + + public_dns_servers = global_config.get_fairgame_config().get("public_dns_servers") + resolutions = 0 + endpoints = set() + + # Resolve the domain for each DNS server to find out how many end points we have + for provider in public_dns_servers: + # Provider is Google, Verisign, etc. + log.info(f"Testing {provider}") + for server in public_dns_servers[provider]: + # Server is 8.8.8.8 or 1.1.1.1 + my_resolver = dns.resolver.Resolver() + my_resolver.nameservers = [server] + + resolved = my_resolver.resolve(domain) + for rdata in resolved: + ipv4_address = rdata.address + endpoints.add(ipv4_address) + resolutions += 1 + log.debug(f"{domain} resolves to {ipv4_address} via {server}") + return endpoints, resolutions + + +@click.command() +@click.option("--domain", help="Specify the domain you want to find endpoints for.") +def show_traceroutes(domain): + if not domain: + log.error("You must specify a domain to test routes using --domain.") + exit(0) + + # Get the endpoints to test + endpoints, resolutions = resolve_domain(domain=domain) + + if platform.system() == "Windows": + trace_command = "tracert -d " + else: + trace_command = "traceroute -n " + + # Spitball test routes via Python's traceroute + for endpoint in endpoints: + log.info(f" {trace_command}{endpoint}") + + signal(SIGINT, handler) main.add_command(amazon) main.add_command(bestbuy) main.add_command(test_notifications) main.add_command(show) +main.add_command(find_endpoints) +main.add_command(show_traceroutes) # Global scope stuff here if is_latest(): diff --git a/common/globalconfig.py b/common/globalconfig.py index 5b021988..addc3d4f 100644 --- a/common/globalconfig.py +++ b/common/globalconfig.py @@ -69,6 +69,9 @@ def get_amazon_config(self, encryption_pass=None): ) return amazon_config + def get_fairgame_config(self): + return self.fairgame_config + def get_browser_profile_path(self): if not self.profile_path: self.profile_path = os.path.join( @@ -76,3 +79,8 @@ def get_browser_profile_path(self): self.global_config["FAIRGAME"].get("profile_name", ".profile-amz"), ) return self.profile_path + + def get_property(self, property_name): + if property_name not in self.global_config.keys(): # we don't want KeyError + return None # just return None if not found + return self.global_config[property_name] diff --git a/config/fairgame.conf b/config/fairgame.conf index 6bfd9af1..5862f55a 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -1,6 +1,36 @@ { "FAIRGAME": { - "profile_name": ".profile-amz" + "profile_name": ".profile-amz", + "public_dns_servers": { + "Google": [ + "8.8.8.8", + "8.8.4.4" + ], + "Cloudflare": [ + "1.1.1.1", + "1.0.0.1" + ], + "Comcast": [ + "75.75.75.75", + "75.75.76.76" + ], + "Quad 9": [ + "9.9.9.9", + "149.112.112.112" + ], + "Comodo": [ + "8.26.56.26", + "8.20.247.20" + ], + "Verisign": [ + "64.6.64.6", + "64.6.65.6" + ], + "OpenDNS": [ + "208.67.222.222", + "208.67.220.220" + ] + } }, "AMAZON": { "SIGN_IN_TEXT": [ From 3bee4975ea1b4a4d7fa5d8ce1802ebaf632149f8 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 20 Feb 2021 08:58:30 -0500 Subject: [PATCH 090/150] --Updated dependencies for endpoint tests --- Pipfile | 1 + cli/cli.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 931e2fcb..5eb8b76c 100644 --- a/Pipfile +++ b/Pipfile @@ -35,6 +35,7 @@ stdiomask = "*" packaging = "*" config = "*" lxml = "*" +dnspython = "*" [requires] python_version = "3.8" diff --git a/cli/cli.py b/cli/cli.py index bbba1cd4..2b61b30b 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -18,13 +18,12 @@ # https://github.com/Hari-Nagarajan/fairgame import os -import shutil import platform +import shutil from datetime import datetime from functools import wraps from pathlib import Path from signal import signal, SIGINT -from icmplib import ping, multiping, traceroute, resolve, Host, Hop LICENSE_PATH = os.path.join( "cli", From bcd3fb559c2c1808f8957a04b698c79d377e1bcf Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 20 Feb 2021 18:35:45 -0500 Subject: [PATCH 091/150] --Cleaned up missing module trapping --Cleaned up imports --Cleaned up interrupt handler --Added trapping for failed DNS resolution --Added AT&T DNS Servers --- Pipfile | 1 - app.py | 18 ++++++++++++-- cli/cli.py | 59 +++++++++++++++++++++++++------------------- config/fairgame.conf | 4 +++ 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/Pipfile b/Pipfile index 5eb8b76c..d8612cca 100644 --- a/Pipfile +++ b/Pipfile @@ -8,7 +8,6 @@ pyinstaller = "*" [packages] requests = "==2.24.0" -click = "*" selenium = "*" chromedriver-py = "==88.0.4324.96" furl = "*" diff --git a/app.py b/app.py index 8dc8ea53..9d0eeb79 100644 --- a/app.py +++ b/app.py @@ -48,8 +48,22 @@ def sha256sum(filename): exit(0) -from cli import cli +def notfound_message(exception): + print(exception) + print(f"Missing '{exception.name}' module. If you ran 'pipenv install', try 'pipenv install {exception.name}'") + print("Exiting...") +try: + from cli import cli +except ModuleNotFoundError as e: + notfound_message(e) + exit(0) + if __name__ == "__main__": - cli.main() + try: + cli.main() + except ModuleNotFoundError as e: + notfound_message(e) + exit(0) + diff --git a/cli/cli.py b/cli/cli.py index 2b61b30b..a8dfc525 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -20,30 +20,25 @@ import os import platform import shutil +import time from datetime import datetime from functools import wraps from pathlib import Path -from signal import signal, SIGINT +from signal import getsignal, SIGINT, signal -LICENSE_PATH = os.path.join( - "cli", - "license", -) - -try: - import click -except ModuleNotFoundError as e: - print(e) - print("Install the missing module noted above.") - exit(0) -import time +import click +from common.globalconfig import AMAZON_CREDENTIAL_FILE, GlobalConfig from notifications.notifications import NotificationHandler, TIME_FORMAT -from utils.logger import log -from common.globalconfig import GlobalConfig, AMAZON_CREDENTIAL_FILE -from utils.version import is_latest, version from stores.amazon import Amazon from stores.bestbuy import BestBuyHandler +from utils.logger import log +from utils.version import is_latest, version + +LICENSE_PATH = os.path.join( + "cli", + "license", +) def get_folder_size(folder): @@ -58,8 +53,9 @@ def sizeof_fmt(num, suffix="B"): return "%.1f%s%s" % (num, "Yi", suffix) -def handler(signal, frame): - log.info("Caught the stop, exiting.") +# see https://docs.python.org/3/library/signal.html +def interrupt_handler(signal_num, frame): + log.info(f"Caught the interrupt signal. Exiting.") exit(0) @@ -70,7 +66,7 @@ def decorator(*args, **kwargs): func(*args, **kwargs) except KeyboardInterrupt: pass - except: + else: notification_handler.send_notification(f"FairGame has crashed.") raise @@ -300,6 +296,7 @@ def test_notifications(disable_sound): @click.option("--w", is_flag=True) @click.option("--c", is_flag=True) def show(w, c): + show_file = "show_c.txt" if w and c: print("Choose one option. Program Quitting") exit(0) @@ -327,7 +324,10 @@ def show(w, c): @click.command() -@click.option("--domain", help="Specify the domain you want to find endpoints for.") +@click.option( + "--domain", + help="Specify the domain you want to find endpoints for (e.g. www.amazon.de, www.amazon.com, smile.amazon.com.", +) def find_endpoints(domain): import dns.resolver @@ -336,9 +336,13 @@ def find_endpoints(domain): exit(0) # Default my_resolver = dns.resolver.Resolver() - resolved = my_resolver.resolve(domain) - for rdata in resolved: - log.info(f"Your computer resolves {domain} to {rdata.address}") + try: + resolved = my_resolver.resolve(domain) + for rdata in resolved: + log.info(f"Your computer resolves {domain} to {rdata.address}") + except Exception as e: + log.error(f"Failed to use local resolver due to: {e}") + exit(1) # Find endpoints from various DNS servers endpoints, resolutions = resolve_domain(domain) @@ -368,7 +372,11 @@ def resolve_domain(domain): my_resolver = dns.resolver.Resolver() my_resolver.nameservers = [server] - resolved = my_resolver.resolve(domain) + try: + resolved = my_resolver.resolve(domain) + except Exception as e: + log.warning(f"Unable to resolve using {provider} server {server} due to: {e}") + continue for rdata in resolved: ipv4_address = rdata.address endpoints.add(ipv4_address) @@ -397,7 +405,8 @@ def show_traceroutes(domain): log.info(f" {trace_command}{endpoint}") -signal(SIGINT, handler) +# Register Signal Handler for Interrupt +signal(SIGINT, interrupt_handler) main.add_command(amazon) main.add_command(bestbuy) diff --git a/config/fairgame.conf b/config/fairgame.conf index 5862f55a..36426048 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -29,6 +29,10 @@ "OpenDNS": [ "208.67.222.222", "208.67.220.220" + ], + "AT&T": [ + "68.94.156.1", + "68.94.157.1" ] } }, From b640129ef1f5a7be9408a822ef6da53f3a83c80e Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 20 Feb 2021 19:41:46 -0500 Subject: [PATCH 092/150] --Removed private DNS servers (AT&T and Comcast) --Added messaging --- cli/cli.py | 9 ++++++--- config/fairgame.conf | 37 +++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/cli/cli.py b/cli/cli.py index a8dfc525..dcdb7fd0 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -24,7 +24,7 @@ from datetime import datetime from functools import wraps from pathlib import Path -from signal import getsignal, SIGINT, signal +from signal import SIGINT, signal import click @@ -334,7 +334,8 @@ def find_endpoints(domain): if not domain: log.error("You must specify a domain to resolve for endpoints with --domain.") exit(0) - # Default + log.info(f"Attempting to resolve '{domain}'") + # Default my_resolver = dns.resolver.Resolver() try: resolved = my_resolver.resolve(domain) @@ -375,7 +376,9 @@ def resolve_domain(domain): try: resolved = my_resolver.resolve(domain) except Exception as e: - log.warning(f"Unable to resolve using {provider} server {server} due to: {e}") + log.warning( + f"Unable to resolve using {provider} server {server} due to: {e}" + ) continue for rdata in resolved: ipv4_address = rdata.address diff --git a/config/fairgame.conf b/config/fairgame.conf index 36426048..839a7151 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -2,37 +2,38 @@ "FAIRGAME": { "profile_name": ".profile-amz", "public_dns_servers": { - "Google": [ - "8.8.8.8", - "8.8.4.4" - ], "Cloudflare": [ "1.1.1.1", "1.0.0.1" ], - "Comcast": [ - "75.75.75.75", - "75.75.76.76" - ], - "Quad 9": [ - "9.9.9.9", - "149.112.112.112" - ], "Comodo": [ "8.26.56.26", "8.20.247.20" ], - "Verisign": [ - "64.6.64.6", - "64.6.65.6" + "Google": [ + "8.8.8.8", + "8.8.4.4" + ], + "Level3": [ + "4.2.2.1", + "4.2.2.3", + "4.2.2.5" + ], + "NTT": [ + "129.250.35.250", + "129.250.35.251" ], "OpenDNS": [ "208.67.222.222", "208.67.220.220" ], - "AT&T": [ - "68.94.156.1", - "68.94.157.1" + "Quad 9": [ + "9.9.9.9", + "149.112.112.112" + ], + "Verisign": [ + "64.6.64.6", + "64.6.65.6" ] } }, From 4c6e75adc67c3b1e38f579d7909aa2389e6062a3 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 22 Feb 2021 08:48:58 -0500 Subject: [PATCH 093/150] --Updated to reflect next micro release for tracking --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index d275ce97..d79c8ac9 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.0" +__VERSION = "0.6.1.dev1" version = Version(__VERSION) From 494b712006c285f9a59c856ac32bd74616663991 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 22 Feb 2021 14:46:27 -0500 Subject: [PATCH 094/150] --Added additional free shipping message --- config/fairgame.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 839a7151..493889c0 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -158,7 +158,8 @@ "ENVÍO GRATIS.", "FRI FRAKT", "GRATIS-LIEFERUNG", - "PRIME FREE DELIVERY" + "PRIME FREE DELIVERY", + "DELIVERY AT NO EXTRA COST FOR PRIME MEMBERS" ], "ADDRESS_SELECT": [ "Select a delivery address", From e3398568806a187e7324aff3bdeb0ede5c3534ee Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 22 Feb 2021 15:03:40 -0500 Subject: [PATCH 095/150] --Updated help text --- cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cli.py b/cli/cli.py index dcdb7fd0..8cf83789 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -389,7 +389,7 @@ def resolve_domain(domain): @click.command() -@click.option("--domain", help="Specify the domain you want to find endpoints for.") +@click.option("--domain", help="Specify the domain you want to generate traceroute commands for.") def show_traceroutes(domain): if not domain: log.error("You must specify a domain to test routes using --domain.") From 88433780d81be6be73d273a51b86c8453fb2e785 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 22 Feb 2021 15:31:01 -0500 Subject: [PATCH 096/150] --Added Best Buy warning message --- cli/cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/cli.py b/cli/cli.py index 8cf83789..85cae529 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -256,6 +256,9 @@ def amazon( @click.option("--headless", is_flag=True) @notify_on_crash def bestbuy(sku, headless): + log.warning( + "As stated in the documentation, Best Buy is deprecated due to their anti-bot measures for high demand items." + ) bb = BestBuyHandler( sku, notification_handler=notification_handler, headless=headless ) @@ -389,7 +392,9 @@ def resolve_domain(domain): @click.command() -@click.option("--domain", help="Specify the domain you want to generate traceroute commands for.") +@click.option( + "--domain", help="Specify the domain you want to generate traceroute commands for." +) def show_traceroutes(domain): if not domain: log.error("You must specify a domain to test routes using --domain.") From 6da39032e566580e08ee0d507f894faf062439b0 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 22 Feb 2021 15:33:01 -0500 Subject: [PATCH 097/150] --Updated to include CLI tools --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index 4c388c13..85f28cf6 100644 --- a/README.md +++ b/README.md @@ -470,6 +470,49 @@ pavlok app in the ```pavlok_config.json``` file, you can copy the template from Once you have setup your `apprise_config.json ` you can test it by running `python app.py test-notifications` from within your pipenv shell. This will send a test notification to all configured notification services. +## CLI Tools + +### CDN Endpoints + +The `find-endpoints` tool is designed to help you understand how many website domain endpoints exist for your geography +based on global Content Delivery Networks (CDNs) and your specific network provider. Its purpose is nothing more than to +educate you about variability of the network depending on how your computer resolves a domain. Doing something useful +with this knowledge is beyond the scope of this feature. + +```shell +Usage: app.py find-endpoints [OPTIONS] + +Options: + --domain TEXT Specify the domain you want to find endpoints for (e.g. + www.amazon.de, www.amazon.com, smile.amazon.com. + + --help Show this message and exit. +``` + +Specifying a domain (e.g. www.amazon.com, www.amazon.es, www.google.com, etc.) will generate a list of IP addresses that +various public name servers resolve the name to. Hopefully this is helpful in understanding the variable nature of the +content that different people see. + +### Routes + +The `show_traceroutes` tool is simply a tool that attempts to generate the commands necessary to determine the various +paths that the Fairgame could take to get to a domain, based on who is resolving the domain to an IP. +It uses the [end points](#cdn-endpoints) tool to convert a domain name to the various IPs and generates a list of +commands you can copy and paste into the console to compare routes. + +```shell +Usage: app.py show-traceroutes [OPTIONS] + +Options: + --domain TEXT Specify the domain you want to generate traceroute commands for. + + --help Show this message and exit. +``` + +This is intended for people who feel that they can modify their network situation such that the fastest route is used. +Explaining the Internet and how routing works is beyond the scope of this command, this tool, this projects, and the +developers. + ## Troubleshooting + Re-read this documentation. From d305aa8a82fa55052671eda73b731bfb0a65c2b1 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 22 Feb 2021 15:53:13 -0500 Subject: [PATCH 098/150] --Added click back in --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index d8612cca..5eb8b76c 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ pyinstaller = "*" [packages] requests = "==2.24.0" +click = "*" selenium = "*" chromedriver-py = "==88.0.4324.96" furl = "*" From 32a13794f5f186147e0c05de531fa554f675ba5b Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 22 Feb 2021 16:16:27 -0500 Subject: [PATCH 099/150] --blackd --- app.py | 5 +++-- utils/encryption.py | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 9d0eeb79..8788e9f5 100644 --- a/app.py +++ b/app.py @@ -50,7 +50,9 @@ def sha256sum(filename): def notfound_message(exception): print(exception) - print(f"Missing '{exception.name}' module. If you ran 'pipenv install', try 'pipenv install {exception.name}'") + print( + f"Missing '{exception.name}' module. If you ran 'pipenv install', try 'pipenv install {exception.name}'" + ) print("Exiting...") @@ -66,4 +68,3 @@ def notfound_message(exception): except ModuleNotFoundError as e: notfound_message(e) exit(0) - diff --git a/utils/encryption.py b/utils/encryption.py index 1e729f35..25d85c1e 100644 --- a/utils/encryption.py +++ b/utils/encryption.py @@ -30,7 +30,9 @@ if platform.machine()[:3] == "arm": # Not great, but workable way to detect Raspberry Pi - log.info("Using reduced CPU and Memory cost parameter for encryption for your platform.") + log.info( + "Using reduced CPU and Memory cost parameter for encryption for your platform." + ) CPU_MEM_COST = 10 else: CPU_MEM_COST = 20 From ab25b7b6160716412202de3654c8d05d915ecc34 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Thu, 25 Feb 2021 19:39:22 -0500 Subject: [PATCH 100/150] Update amazon.py Fixed crash on page timeout --- stores/amazon.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 29371d0c..8f01060a 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -862,23 +862,27 @@ def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): f = f"{AMAZON_URLS['ATC_URL']}?OfferListingId.1={offering_id}&Quantity.1=1" atc_attempts = 0 while atc_attempts < max_atc_retries: - with self.wait_for_page_content_change(timeout=5): - self.driver.get(f) - xpath = "//input[@value='add' and @name='add']" - if wait_for_element_by_xpath(self.driver, xpath): - try: - with self.wait_for_page_content_change(timeout=10): - self.driver.find_element_by_xpath(xpath).click() - except sel_exceptions.NoSuchElementException: + try: + with self.wait_for_page_content_change(timeout=5): + self.driver.get(f) + xpath = "//input[@value='add' and @name='add']" + if wait_for_element_by_xpath(self.driver, xpath): + try: + with self.wait_for_page_content_change(timeout=10): + self.driver.find_element_by_xpath(xpath).click() + except sel_exceptions.NoSuchElementException: + log.error("Continue button not present on page") + else: log.error("Continue button not present on page") - else: - log.error("Continue button not present on page") - # verify cart is non-zero - if self.get_cart_count() != 0: - return True - else: - atc_attempts = atc_attempts + 1 + # verify cart is non-zero + if self.get_cart_count() != 0: + return True + else: + atc_attempts = atc_attempts + 1 + except sel_exceptions.TimeoutException: + log.error("Could not load webpage within timeout period") + atc_attempts += 1 return False # search lists of asin lists, and remove the first list that matches provided asin From beb354134ce00707449dba92bdc0023b8e8c0074 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 25 Feb 2021 19:41:35 -0500 Subject: [PATCH 101/150] =?UTF-8?q?--Added=20additional=20title=20for=20Am?= =?UTF-8?q?azon.sg=20--Added=20additional=20Free=20Shipping=20messsage=20"?= =?UTF-8?q?GRATIS=20LIEFERUNG=20F=C3=9CR=20PRIME-MITGLIEDER"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/fairgame.conf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 493889c0..4353178e 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -109,7 +109,8 @@ "Preparing your order", "Ihre Bestellung wird vorbereitet", "Pagamento Amazon.it", - "Ordine in preparazione" + "Ordine in preparazione", + "Amazon.sg Checkout" ], "ORDER_COMPLETE_TITLES": [ "Amazon.com Thanks You", @@ -159,7 +160,8 @@ "FRI FRAKT", "GRATIS-LIEFERUNG", "PRIME FREE DELIVERY", - "DELIVERY AT NO EXTRA COST FOR PRIME MEMBERS" + "DELIVERY AT NO EXTRA COST FOR PRIME MEMBERS", + "GRATIS LIEFERUNG FÜR PRIME-MITGLIEDER" ], "ADDRESS_SELECT": [ "Select a delivery address", From 51acbb4192edd1a800ec51ef6599fa498eb2be17 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Thu, 25 Feb 2021 19:52:17 -0500 Subject: [PATCH 102/150] Update amazon.py added some more shipping bypass buttons that maybe do something --- stores/amazon.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 8f01060a..e61b6864 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1019,10 +1019,20 @@ def navigate_pages(self, test): element = None try: element = self.driver.find_element_by_xpath( - '//*[@class="ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium "]' + '//*[contains(@class,"ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium")]' ) except sel_exceptions.NoSuchElementException: - pass + try: + element = self.driver.find_element_by_xpath( + '//input[@type="submit"]' + ) + except sel_exceptions.NoSuchElementException: + try: + element = self.driver.find_element_by_xpath( + '//*[@id="orderSummaryPrimaryActionBtn"]' + ) + except: + pass if element: log.warning("FairGame thinks it needs to pick a shipping address.") log.warning( From a2c82bcc903b3dad7436d854f911ad68c0770b41 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Thu, 25 Feb 2021 20:09:14 -0500 Subject: [PATCH 103/150] Update amazon.py fixed crash for could not click button when attempting to add to cart --- stores/amazon.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stores/amazon.py b/stores/amazon.py index e61b6864..d7b3a141 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -872,6 +872,11 @@ def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): self.driver.find_element_by_xpath(xpath).click() except sel_exceptions.NoSuchElementException: log.error("Continue button not present on page") + except sel_exceptions.ElementNotInteractableException or sel_exceptions.ElementClickInterceptedException or sel_exceptions.ElementNotVisibleException: + log.error("Could not click button") + except sel_exceptions.WebDriverException as e: + log.error("Selenium Error") + log.error(e) else: log.error("Continue button not present on page") From 1af036ff76f2f2ff6ce064840cf1eb7f7d9bab18 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 09:30:02 -0500 Subject: [PATCH 104/150] Update amazon.py added screenshot taking during atc_attempt selenium exceptions --- stores/amazon.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stores/amazon.py b/stores/amazon.py index d7b3a141..5424c16b 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -874,9 +874,13 @@ def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): log.error("Continue button not present on page") except sel_exceptions.ElementNotInteractableException or sel_exceptions.ElementClickInterceptedException or sel_exceptions.ElementNotVisibleException: log.error("Could not click button") + self.take_screenshots(page="atc-error") + self.save_page_source(page="atc-error") except sel_exceptions.WebDriverException as e: log.error("Selenium Error") log.error(e) + self.take_screenshots(page="atc-error") + self.save_page_source(page="atc-error") else: log.error("Continue button not present on page") From 95bdb72974fc338dcf56479166cb9df99718411c Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 09:37:15 -0500 Subject: [PATCH 105/150] Update amazon.py refactor loop flag name to be more descriptive. reset unknown_title_notification_sent flag at start of each stock_check loop. --- stores/amazon.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 5424c16b..ee983b20 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -242,11 +242,12 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): time.sleep(30) return - keep_going = True + continue_stock_check = True log.info("Checking stock for items.") - while keep_going: + while continue_stock_check: + self.unknown_title_notification_sent = False asin = self.run_asins(delay) # found something in stock and under reserve # initialize loop limiter variables @@ -281,7 +282,7 @@ def run(self, delay=DEFAULT_REFRESH_DELAY, test=False): self.try_to_checkout = False # if no items left it list, let loop end if not self.asin_list: - keep_going = False + continue_stock_check = False runtime = time.time() - self.start_time log.info(f"FairGame bot ran for {runtime} seconds.") time.sleep(10) # add a delay to shut stuff done From e4614d9711df5a5d231c888f92c07ad409201e45 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 10:24:01 -0500 Subject: [PATCH 106/150] Refactoring amazon.py and shipping bypass changes Refactoring: Added do_button_click, will click buttons and handle exceptions for some generic/typical cases Pulled out the unknown title stuff under Address select handler as a separate method. Also call this on unknown page, after doing the preliminary element checks. Created method for address select, called when on page, and when checking elements --- stores/amazon.py | 269 +++++++++++++++++++++++++++-------------------- 1 file changed, 157 insertions(+), 112 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index ee983b20..4725575c 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -908,9 +908,6 @@ def remove_asin_list(self, asin): # checkout page navigator @debug def navigate_pages(self, test): - # delay to wait for page load - # time.sleep(self.page_wait_delay()) - title = self.driver.title log.info(f"Navigating page title: '{title}'") # see if this resolves blank page title issue? @@ -950,23 +947,15 @@ def navigate_pages(self, test): elif title in amazon_config["BUSINESS_PO_TITLES"]: self.handle_business_po() elif title in amazon_config["ADDRESS_SELECT"]: - if not self.unknown_title_notification_sent: - self.notification_handler.play_alarm_sound() - self.send_notification( - "User interaction required for checkout!", - title, - self.take_screenshots, + if self.shipping_bypass: + self.handle_shipping_page() + else: + log.warning( + "Landed on address selection screen. Fairgame will NOT select an address for you. " + "Please select necessary options to arrive at the Review Order Page before the next " + "refresh, or complete checkout manually. You have 30 seconds." ) - self.unknown_title_notification_sent = True - log.warning( - "Landed on address selection screen. Fairgame will NOT select an address for you. " - "Please select necessary options to arrive at the Review Order Page before the next " - "refresh, or complete checkout manually. You have 30 seconds." - ) - for i in range(30, 0, -1): - log.warning(f"{i}...") - time.sleep(1) - return + self.handle_unknown_title(title) else: log.debug(f"title is: [{title}]") # see if we can handle blank titles here @@ -1025,44 +1014,11 @@ def navigate_pages(self, test): except sel_exceptions.ElementNotInteractableException: log.debug("FairGame could not click No Thanks button") + # see if a use this address (or similar) button is on page (based on known xpaths). Only check if + # user has set the shipping_bypass flag if self.shipping_bypass: - element = None - try: - element = self.driver.find_element_by_xpath( - '//*[contains(@class,"ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium")]' - ) - except sel_exceptions.NoSuchElementException: - try: - element = self.driver.find_element_by_xpath( - '//input[@type="submit"]' - ) - except sel_exceptions.NoSuchElementException: - try: - element = self.driver.find_element_by_xpath( - '//*[@id="orderSummaryPrimaryActionBtn"]' - ) - except: - pass - if element: - log.warning("FairGame thinks it needs to pick a shipping address.") - log.warning( - "It will click whichever ship to this address button it found." - ) - log.warning( - "If this works, VERIFY THE ADDRESS IT SHIPPED TO IMMEDIATELY!" - ) - self.send_notification( - message="Clicking ship to address, hopefully this works. VERIFY ASAP!", - page_name="choose-shipping", - take_screenshot=self.take_screenshots, - ) - try: - element.click() - log.info("Clicked button.") - self.wait_for_page_change(page_title=title) - return - except sel_exceptions.WebDriverException: - log.error("Could not click ship to address button") + if self.handle_shipping_page(): + return if self.get_cart_count() == 0: log.info("It appears you have nothing in your cart.") @@ -1080,23 +1036,29 @@ def navigate_pages(self, test): log.error( f"'{title}' is not a known page title. Please create issue indicating the title with a screenshot of page" ) - self.send_notification( - f"Encountered Unknown Page Title: `{title}", - "unknown-title", - self.take_screenshots, - ) - self.save_page_source("unknown-title") - log.info("going to try and redirect to cart page") + # give user 30 seconds to respond + self.handle_unknown_title(title=title) + # check if page title changed, if not, then continue doing other checks: + if self.driver.title != title: + log.info( + "FairGame thinks user intervened in time, will now continue running" + ) + return + else: + log.warning( + "FairGame does not think the user intervened in time, will attempt other methods to continue" + ) + log.info("Going to try and redirect to cart page") try: - self.driver.get(AMAZON_URLS["CART_URL"]) + with self.wait_for_page_content_change(timeout=10): + self.driver.get(AMAZON_URLS["CART_URL"]) except sel_exceptions.WebDriverException: log.error( "failed to load cart URL, refreshing and returning to handler" ) - self.driver.refresh() - time.sleep(3) + with self.wait_for_page_content_change(timeout=10): + self.driver.refresh() return - self.wait_for_page_change(page_title=title) time.sleep(1) # wait a second for page to load # verify cart quantity is not zero # note, not using greater than 0, in case there is an error, @@ -1119,25 +1081,88 @@ def navigate_pages(self, test): except sel_exceptions.NoSuchElementException: pass if time.time() > timeout: - log.error( - "Could not find and click button, refreshing and returning to handler" - ) - self.driver.refresh() - time.sleep(3) + log.error("Could not find and click button") break if button: try: - current_title = self.driver.title log.info("Found ptc button, attempting to click.") - button.click() - log.info("Clicked ptc button") - self.wait_for_page_change(page_title=current_title) + with self.wait_for_page_content_change(timeout=10): + button.click() + log.info("Clicked ptc button") except sel_exceptions.WebDriverException: log.info( "Could not click button - refreshing and returning to checkout handler" ) - self.driver.refresh() - time.sleep(3) + with self.wait_for_page_content_change(): + self.driver.refresh() + return + + # if we made it this far, all attempts to handle page failed, get current page info and return to handler + log.error( + "FairGame could not navigate current page, refreshing and returning to handler" + ) + self.save_page_source(page="unknown") + self.save_screenshot(page="unknown") + with self.wait_for_page_content_change(): + self.driver.refresh() + return + + def handle_unknown_title(self, title): + if not self.unknown_title_notification_sent: + self.notification_handler.play_alarm_sound() + self.send_notification( + "User interaction required for checkout! You have 30 seconds!", + title, + self.take_screenshots, + ) + self.unknown_title_notification_sent = True + for i in range(30, 0, -1): + log.warning(f"{i}...") + time.sleep(1) + return + + # Method to try and click the handle shipping page + def handle_shipping_page(self): + element = None + try: + element = self.driver.find_element_by_xpath( + '//*[contains(@class,"ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium")]' + ) + except sel_exceptions.NoSuchElementException: + try: + element = self.driver.find_element_by_xpath('//input[@type="submit"]') + except sel_exceptions.NoSuchElementException: + try: + element = self.driver.find_element_by_xpath( + '//*[@id="orderSummaryPrimaryActionBtn"]' + ) + except sel_exceptions.NoSuchElementException: + pass + if element: + log.warning("FairGame thinks it needs to pick a shipping address.") + log.warning("It will click whichever ship to this address button it found.") + log.warning("If this works, VERIFY THE ADDRESS IT SHIPPED TO IMMEDIATELY!") + self.send_notification( + message="Clicking ship to address, hopefully this works. VERIFY ASAP!", + page_name="choose-shipping", + take_screenshot=self.take_screenshots, + ) + try: + with self.wait_for_page_content_change(timeout=10): + element.click() + log.info("Clicked button.") + return True + except sel_exceptions.WebDriverException as e: + log.error("Could not click ship to address button") + log.error(e) + self.save_screenshot(page="shipping-select-error") + self.save_page_source(page="shipping-select-error") + else: + log.error("FairGame cannot find a button to click on the shipping page") + self.save_screenshot(page="shipping-select-error") + self.save_page_source(page="shipping-select-error") + # if we make it this far, it failed to click button + return False # returns negative number if cart element does not exist, returns number if cart exists def get_cart_count(self): @@ -1176,24 +1201,45 @@ def handle_prime_signup(self): self.take_screenshots, ) if button: - current_page = self.driver.title - button.click() - self.wait_for_page_change(current_page) - else: - log.error("Prime offer page popped up, user intervention required") - self.notification_handler.play_alarm_sound() - self.notification_handler.send_notification( - "Prime offer page popped up, user intervention required" - ) - timeout = self.get_timeout(timeout=60) - while self.driver.title in amazon_config["PRIME_TITLES"]: - if time.time() > timeout: - log.info( - "user did not intervene in time, will try and refresh page" - ) + if self.do_button_click( + button=button, + clicking_text="Attempting to click No Thanks button on Prime Signup Page", + fail_text="Failed to click No Thanks button on Prime Signup Page", + ): + return + + # If we get to this point, there was either no button, or we couldn't click it (exception hit above) + log.error("Prime offer page popped up, user intervention required") + self.notification_handler.play_alarm_sound() + self.notification_handler.send_notification( + "Prime offer page popped up, user intervention required" + ) + timeout = self.get_timeout(timeout=60) + while self.driver.title in amazon_config["PRIME_TITLES"]: + if time.time() > timeout: + log.info("user did not intervene in time, will try and refresh page") + with self.wait_for_page_content_change(): self.driver.refresh() - time.sleep(DEFAULT_MAX_WEIRD_PAGE_DELAY) - break + break + time.sleep(0.5) + + def do_button_click( + self, + button, + clicking_text="Clicking button", + clicked_text="Button clicked", + fail_text="Could not click button", + ): + try: + with self.wait_for_page_content_change(): + log.info(clicking_text) + button.click() + log.info(clicked_text) + return True + except sel_exceptions.WebDriverException as e: + log.error(fail_text) + log.error(e) + return False @debug def handle_home_page(self): @@ -1205,23 +1251,22 @@ def handle_home_page(self): log.info("Could not find cart button") current_page = self.driver.title if button: - button.click() - self.wait_for_page_change(current_page) - else: - self.send_notification( - "Could not click cart button, user intervention required", - "home-page-error", - self.take_screenshots, - ) - timeout = self.get_timeout(timeout=300) - while self.driver.title == current_page: - time.sleep(0.25) - if time.time() > timeout: - log.info( - "user failed to intervene in time, returning to stock check" - ) - self.try_to_checkout = False - break + if self.do_button_click(button=button): + return + + # no button found or could not interact with the button + self.send_notification( + "Could not click cart button, user intervention required", + "home-page-error", + self.take_screenshots, + ) + timeout = self.get_timeout(timeout=300) + while self.driver.title == current_page: + time.sleep(0.25) + if time.time() > timeout: + log.info("user failed to intervene in time, returning to stock check") + self.try_to_checkout = False + break @debug def handle_cart(self): From 0c85e10587cb23bb9359162242d997a719a8f202 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 10:48:28 -0500 Subject: [PATCH 107/150] amazon.py updates added join xpath function, to join several xpaths together for checking put the address button xpaths in fairgame.conf refactored a few items to use wait_for_page_change_content. added check for shipping buttons during handle_cart (if shipping bypass selected) updated version number to dev2 --- config/fairgame.conf | 5 +++++ stores/amazon.py | 27 +++++++++++++++++++-------- utils/version.py | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 4353178e..199c4f03 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -168,5 +168,10 @@ "Ordine in preparazione", "Select a shipping address" ] + "XPATH_ADDRESS":[ + '//*[contains(@class,"ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium")]', + '//input[@type="submit"]', + '//*[@id="orderSummaryPrimaryActionBtn"]' + ] } } diff --git a/stores/amazon.py b/stores/amazon.py index 4725575c..1246558c 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1289,7 +1289,16 @@ def handle_cart(self): button = self.driver.find_element_by_xpath('//*[@id="hlb-ptc-btn"]') break except sel_exceptions.NoSuchElementException: - pass + # also check for address buttons here? + if self.shipping_bypass(): + try: + button = self.driver.find_element_by_xpath( + join_xpaths(amazon_config["XPATH_ADDRESS"]) + ) + break + except sel_exceptions.NoSuchElementException: + pass + if time.time() > timeout: log.info("couldn't find buttons to proceed to checkout") self.save_page_source("ptc-error") @@ -1311,7 +1320,6 @@ def handle_cart(self): self.checkout_retry += 1 return - current_page = self.driver.title if button: log.info("Found Checkout Button") if self.detailed: @@ -1321,15 +1329,14 @@ def handle_cart(self): take_screenshot=self.take_screenshots, ) try: - button.click() - log.info("Clicked Proceed to Checkout Button") - self.wait_for_page_change(page_title=current_page, timeout=7) + with self.wait_for_page_content_change(): + self.do_button_click(button=button) except sel_exceptions.WebDriverException: log.error("Problem clicking Proceed to Checkout button.") log.info("Refreshing page to try again") - self.driver.refresh() - self.wait_for_page_change(page_title=current_page) - self.checkout_retry += 1 + with self.wait_for_page_content_change(): + self.driver.refresh() + self.checkout_retry += 1 @debug def handle_checkout(self, test): @@ -1862,3 +1869,7 @@ def wait_for_element_by_xpath(d, xpath, timeout=10): return False return True + + +def join_xpaths(xpath_list, separator=" | "): + return separator.join(xpath_list) diff --git a/utils/version.py b/utils/version.py index d79c8ac9..fe69b50e 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.1.dev1" +__VERSION = "0.6.1.dev2" version = Version(__VERSION) From a58b18bc9873c526242412dcbf0eef172119f2f6 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 10:56:01 -0500 Subject: [PATCH 108/150] Update README.md fixed multiple instances FAQ response --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 85f28cf6..74bc7a63 100644 --- a/README.md +++ b/README.md @@ -546,9 +546,7 @@ To keep up with questions, the Discord channel [#FAQ](https://discord.gg/GEsarYK answers. If you don't find it there, ask in #tech-support. 1. **Can I run multiple instances of the bot?** - - Yes. For example you can run one instance to check stock on Best Buy and a separate instance to check stock on - Amazon. Bear in mind that if you do this you may end up with multiple purchases going through at the same time. + While possible, running multiple instances are not a supported usage case. You are on your own to figure this one out. 2. **Does Fairgame automatically bypass CAPTCHA's on the store sites?** For Amazon, yes. The bot will try and auto-solve CAPTCHA's during the checkout process. From 64b56da8e3f3cb25fe84dee4511928f654c0ab30 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 10:57:54 -0500 Subject: [PATCH 109/150] Update fairgame.conf --- config/fairgame.conf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 199c4f03..d717152f 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -110,7 +110,8 @@ "Ihre Bestellung wird vorbereitet", "Pagamento Amazon.it", "Ordine in preparazione", - "Amazon.sg Checkout" + "Amazon.sg Checkout", + "A preparar o seu pedido" ], "ORDER_COMPLETE_TITLES": [ "Amazon.com Thanks You", @@ -124,7 +125,8 @@ "Grazie da Amazon.it", "Hartelijk dank", "Thank You", - "Amazon.de Vielen Dank" + "Amazon.de Vielen Dank", + "Obrigado, o seu pedido foi efetuado" ], "BUSINESS_PO_TITLES": [ "Business order information" From 8f7aff6bfb949db7b8daa44b436acb8080fdb1f6 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 26 Feb 2021 12:21:14 -0500 Subject: [PATCH 110/150] --updated "continue" button click mechanics to prevent clicking buttons that aren't (yet?) available --- stores/amazon.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 1246558c..a2fd69f1 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -88,7 +88,7 @@ DEFAULT_MAX_TIMEOUT = 10 DEFAULT_MAX_URL_FAIL = 5 -amazon_config = None +amazon_config = {} class Amazon: @@ -869,18 +869,26 @@ def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): xpath = "//input[@value='add' and @name='add']" if wait_for_element_by_xpath(self.driver, xpath): try: - with self.wait_for_page_content_change(timeout=10): - self.driver.find_element_by_xpath(xpath).click() + with self.wait_for_page_content_change(timeout=15): + continue_btn = WebDriverWait( + self.driver, timeout=5 + ).until(EC.element_to_be_clickable((By.XPATH, xpath))) + continue_btn.click() except sel_exceptions.NoSuchElementException: log.error("Continue button not present on page") - except sel_exceptions.ElementNotInteractableException or sel_exceptions.ElementClickInterceptedException or sel_exceptions.ElementNotVisibleException: - log.error("Could not click button") - self.take_screenshots(page="atc-error") + except ( + sel_exceptions.ElementNotInteractableException, + sel_exceptions.ElementClickInterceptedException, + sel_exceptions.ElementNotVisibleException, + sel_exceptions.TimeoutException, + ) as e: + log.error(f"Could not click button due to {e}") + self.save_screenshot(page="atc-continue-button`") self.save_page_source(page="atc-error") except sel_exceptions.WebDriverException as e: log.error("Selenium Error") log.error(e) - self.take_screenshots(page="atc-error") + self.save_screenshot(page="atc-error") self.save_page_source(page="atc-error") else: log.error("Continue button not present on page") From eb0825589b04ae8ef6cc49c2407cb475c92d9ea0 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 26 Feb 2021 12:22:27 -0500 Subject: [PATCH 111/150] --Synchronized the page error file names for exceptions --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index a2fd69f1..aa5bf0ac 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -884,7 +884,7 @@ def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): ) as e: log.error(f"Could not click button due to {e}") self.save_screenshot(page="atc-continue-button`") - self.save_page_source(page="atc-error") + self.save_page_source(page="atc-continue-button`") except sel_exceptions.WebDriverException as e: log.error("Selenium Error") log.error(e) From 2078334c3462179a01427a506b97e995d04501dc Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 15:12:27 -0500 Subject: [PATCH 112/150] Update README.md updates regarding ASINs --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 74bc7a63..d6b985c5 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ our Wiki . You *can* use the "Download Zip" button on the GitHub repository's ho more difficult. If you can get setup with the GitHub Desktop app, updating to the latest version of the bot takes 1 click. -!!! YOU WILL NEED TO USE THE 3.8 BRANCH OF PYTHON, 3.9.0 BREAKS DEPENDENCIES !!! +**!!! YOU WILL NEED TO USE THE 3.8 BRANCH OF PYTHON, 3.9.X BREAKS DEPENDENCIES !!!** It is best if you use the newest version (3.8.7) but 3.8.5 and 3.8.6 should also work. 3.8.0 does not. @@ -247,18 +247,17 @@ Options: Make a copy of `amazon_config.template_json` and rename to `amazon_config.json`. Edit it according to the ASINs you are interested in purchasing. [*What's an -ASIN?*](https://www.datafeedwatch.com/blog/amazon-asin-number-what-is-it-and-how-do-you-get-it#how-to-find-asin) +ASIN?*](https://www.datafeedwatch.com/blog/amazon-asin-number-what-is-it-and-how-do-you-get-it#how-to-find-asin) You can find a list of ASINs for some common products people are looking for in the [cheat sheet](https://docs.google.com/document/d/14kZ0SNC97DFVRStnrdsJ8xbQO1m42v7svy93kUdtX48). If it's not in the cheat sheet, you have to look it up yourself. * `asin_groups` indicates the number of ASIN groups you want to use. -* `asin_list_x` list of ASINs for products you want to purchase. You must locate these (see Discord or lookup the ASIN - on product pages). +* `asin_list_x` list of ASINs for products you want to purchase. You must locate these for the products you want, use the links above to get started. * The first time an item from list "x" is in stock and under its associated reserve, it will purchase it. * If the purchase is successful, the bot will not buy anything else from list "x". * Use sequential numbers for x, starting from 1. x can be any integer from 1 to 18,446,744,073,709,551,616 * `reserve_min_x` set a minimum limit to consider for purchasing an item. If a seller has a listing for a 700 dollar item a 1 dollar, it's likely fake. * `reserve_max_x` is the most amount you want to spend for a single item (i.e., ASIN) in `asin_list_x`. Does not include - tax. If --checkshipping flag is active, this includes shipping listed on offer page. + tax. If `--checkshipping` flag is active, this includes shipping listed on offer page. * `amazon_website` amazon domain you want to use. smile subdomain appears to work better, if available in your country. [*What is Smile?*](https://org.amazon.com/) From 732e6699ee4b072b8ee7372eb152606b30f57874 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 16:10:07 -0500 Subject: [PATCH 113/150] Update amazon.py added exception for open_offers_link.click() Selenium - why do we have to use try/except for everything?! --- stores/amazon.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index aa5bf0ac..17bd2815 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -557,9 +557,14 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): elif offers.get_attribute("data-action") == "show-all-offers-display": # PDP Page # Find the offers link first, just to burn some cycles in case the flyout is loading - open_offers_link: WebElement = self.driver.find_element_by_xpath( - "//span[@data-action='show-all-offers-display']//a" - ) + try: + open_offers_link: WebElement = ( + self.driver.find_element_by_xpath( + "//span[@data-action='show-all-offers-display']//a" + ) + ) + except sel_exceptions.NoSuchElementException: + pass # Now check to see if we're already loading the flyout... flyout = self.driver.find_elements_by_xpath( @@ -580,7 +585,15 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): continue log.debug("Attempting to click the open offers link...") - open_offers_link.click() + try: + open_offers_link.click() + except sel_exceptions.WebDriverException as e: + log.error("Problem clicking open offers link") + log.error("May have issue with rest of this ASIN check cycle") + log.debug(e) + filename = "open-offers-link-error" + self.save_screenshot(filename) + self.save_page_source(filename) try: # Now wait for the flyout to load log.debug("Waiting for flyout...") From a8d0bdf383e0852ebf5d8ad63fb3b4f23aab02d1 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 17:42:00 -0500 Subject: [PATCH 114/150] amazon.py updates - xpaths to conf file Started moving xpaths to the fairgame.conf file. Created get_amazon_element method which will do the whole get element by xpath method, with the key for the xpath as an input variable. --- config/fairgame.conf | 24 ++++++- stores/amazon.py | 165 ++++++++++++++++++++----------------------- 2 files changed, 97 insertions(+), 92 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index d717152f..32b8af36 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -170,10 +170,30 @@ "Ordine in preparazione", "Select a shipping address" ] - "XPATH_ADDRESS":[ + "XPATHS": + { + "ADDRESS_SELECT":[ '//*[contains(@class,"ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium")]', '//input[@type="submit"]', '//*[@id="orderSummaryPrimaryActionBtn"]' - ] + ], + "PRIME_NO_THANKS":[ + '//*[contains(@class, "no-thanks-button")]', + '//*[contains(@class, "prime-nothanks-button")]', + '//*[contains(@class, "prime-no-button")]' + ], + "CART":[ + '//*[@id="nav-cart-count"]' + ] + "PTC":[ + '//*[@id="hlb-ptc-btn-native"]', + '//input[@name="proceedToRetailCheckout"]', + '//*[@id="hlb-ptc-btn"]', + '//*[@id="sc-buy-box-ptc-button"]' + ] + "ATC":[ + "//div[@id='aod-pinned-offer' or @id='aod-offer' or @id='olpOfferList']//input[@name='submit.addToCart']" + ] + } } } diff --git a/stores/amazon.py b/stores/amazon.py index 17bd2815..ec6d99b1 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -89,6 +89,7 @@ DEFAULT_MAX_URL_FAIL = 5 amazon_config = {} +amazon_xpath = {} class Amazon: @@ -144,6 +145,7 @@ def __init__( from cli.cli import global_config amazon_config = global_config.get_amazon_config(encryption_pass) + amazon_xpath = amazon_config["XPATHS"] self.profile_path = global_config.get_browser_profile_path() try: @@ -510,6 +512,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False timeout = self.get_timeout() + atc_buttons = None while True: # Sanity check to see if we have any offers try: @@ -557,6 +560,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): elif offers.get_attribute("data-action") == "show-all-offers-display": # PDP Page # Find the offers link first, just to burn some cycles in case the flyout is loading + open_offers_link = None try: open_offers_link: WebElement = ( self.driver.find_element_by_xpath( @@ -584,31 +588,38 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) continue - log.debug("Attempting to click the open offers link...") - try: - open_offers_link.click() - except sel_exceptions.WebDriverException as e: - log.error("Problem clicking open offers link") - log.error("May have issue with rest of this ASIN check cycle") - log.debug(e) - filename = "open-offers-link-error" - self.save_screenshot(filename) - self.save_page_source(filename) - try: - # Now wait for the flyout to load - log.debug("Waiting for flyout...") - WebDriverWait(self.driver, timeout=DEFAULT_MAX_TIMEOUT).until( - lambda d: d.find_element_by_xpath( - "//div[@id='aod-container'] | //div[@id='olpOfferList']" + if open_offers_link: + log.debug("Attempting to click the open offers link...") + try: + open_offers_link.click() + except sel_exceptions.WebDriverException as e: + log.error("Problem clicking open offers link") + log.error( + "May have issue with rest of this ASIN check cycle" ) - ) - log.debug("Flyout should be open and populated.") - except sel_exceptions.TimeoutException as te: - log.error( - "Timed out waiting for the flyout to open and populate. Is the " - "connection slow? Do you see the flyout populate?" - ) - continue + log.debug(e) + filename = "open-offers-link-error" + self.save_screenshot(filename) + self.save_page_source(filename) + try: + # Now wait for the flyout to load + log.debug("Waiting for flyout...") + WebDriverWait( + self.driver, timeout=DEFAULT_MAX_TIMEOUT + ).until( + lambda d: d.find_element_by_xpath( + "//div[@id='aod-container'] | //div[@id='olpOfferList']" + ) + ) + log.debug("Flyout should be open and populated.") + except sel_exceptions.TimeoutException as te: + log.error( + "Timed out waiting for the flyout to open and populate. Is the " + "connection slow? Do you see the flyout populate?" + ) + continue + else: + log.error("Could not open offers link") elif ( offers.get_attribute("aria-labelledby") == "submit.add-to-cart-announce" @@ -656,9 +667,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) continue - atc_buttons: WebElement = self.driver.find_elements_by_xpath( - "//div[@id='aod-pinned-offer' or @id='aod-offer' or @id='olpOfferList']//input[@name='submit.addToCart']" - ) + atc_buttons: WebElement = self.get_amazon_element(key="ATC") # if not atc_buttons: # # Sanity check to see if we have a valid page, but no offers: # offer_count = WebDriverWait(self.driver, timeout=25).until( @@ -1018,23 +1027,17 @@ def navigate_pages(self, test): element = None # Prime offer page? try: - element = self.driver.find_element_by_xpath( - '//*[contains(@class, "no-thanks-button") or contains(@class, "prime-nothanks-button") or contains(@class, "prime-no-button")]' - ) + element = self.get_amazon_element(key="PRIME_NO_THANKS") except sel_exceptions.NoSuchElementException: pass if element: - try: - log.info( - "FairGame thinks it is seeing a Prime Offer, attempting to click No Thanks" - ) - element.click() - self.wait_for_page_change(page_title=title) - # if we were able to click, return to program flow + if self.do_button_click( + button=element, + clicking_text="FairGame thinks it is seeing a Prime Offer, attempting to click No Thanks", + fail_text="FairGame could not click No Thanks button", + debug=True, + ): return - except sel_exceptions.ElementNotInteractableException: - log.debug("FairGame could not click No Thanks button") - # see if a use this address (or similar) button is on page (based on known xpaths). Only check if # user has set the shipping_bypass flag if self.shipping_bypass: @@ -1092,28 +1095,24 @@ def navigate_pages(self, test): log.info("trying to click proceed to checkout") timeout = self.get_timeout() - button = [] while True: try: - button = self.driver.find_element_by_xpath( - '//*[@id="sc-buy-box-ptc-button"]' - ) + button = self.get_amazon_element(key="PTC") break except sel_exceptions.NoSuchElementException: - pass + button = None if time.time() > timeout: log.error("Could not find and click button") break if button: - try: - log.info("Found ptc button, attempting to click.") - with self.wait_for_page_content_change(timeout=10): - button.click() - log.info("Clicked ptc button") - except sel_exceptions.WebDriverException: - log.info( - "Could not click button - refreshing and returning to checkout handler" - ) + if self.do_button_click( + button=button, + clicking_text="Found ptc button, attempting to click.", + clicked_text="Clicked ptc button", + fail_text="Could not click button", + ): + return + else: with self.wait_for_page_content_change(): self.driver.refresh() return @@ -1146,19 +1145,9 @@ def handle_unknown_title(self, title): def handle_shipping_page(self): element = None try: - element = self.driver.find_element_by_xpath( - '//*[contains(@class,"ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium")]' - ) + element = self.get_amazon_element(key="ADDRESS_SELECT") except sel_exceptions.NoSuchElementException: - try: - element = self.driver.find_element_by_xpath('//input[@type="submit"]') - except sel_exceptions.NoSuchElementException: - try: - element = self.driver.find_element_by_xpath( - '//*[@id="orderSummaryPrimaryActionBtn"]' - ) - except sel_exceptions.NoSuchElementException: - pass + pass if element: log.warning("FairGame thinks it needs to pick a shipping address.") log.warning("It will click whichever ship to this address button it found.") @@ -1185,11 +1174,14 @@ def handle_shipping_page(self): # if we make it this far, it failed to click button return False + def get_amazon_element(self, key): + return self.driver.find_element_by_xpath(join_xpaths(amazon_xpath[key])) + # returns negative number if cart element does not exist, returns number if cart exists def get_cart_count(self): # check if cart number is on the page, if cart items = 0 try: - element = self.driver.find_element_by_xpath('//*[@id="nav-cart-count"]') + element = self.get_amazon_element(key="CART") except sel_exceptions.NoSuchElementException: return -1 if element: @@ -1208,10 +1200,7 @@ def handle_prime_signup(self): ) # just doing manual wait, sign up for prime if you don't want to deal with this button = None try: - button = self.driver.find_element_by_xpath( - # '//*[@class="a-button a-button-base no-thanks-button"]' - '//*[contains(@class, "no-thanks-button") or contains(@class, "prime-nothanks-button") or contains(@class, "prime-no-button")]' - ) + button = self.get_amazon_element(key="PRIME_NO_THANKS") except sel_exceptions.NoSuchElementException: log.error("could not find button") log.info("sign up for Prime and this won't happen anymore") @@ -1250,6 +1239,7 @@ def do_button_click( clicking_text="Clicking button", clicked_text="Button clicked", fail_text="Could not click button", + debug=False, ): try: with self.wait_for_page_content_change(): @@ -1258,8 +1248,12 @@ def do_button_click( log.info(clicked_text) return True except sel_exceptions.WebDriverException as e: - log.error(fail_text) - log.error(e) + if debug: + log.debug(fail_text) + log.debug(e) + else: + log.error(fail_text) + log.error(e) return False @debug @@ -1267,7 +1261,7 @@ def handle_home_page(self): log.info("On home page, trying to get back to checkout") button = None try: - button = self.driver.find_element_by_xpath('//*[@id="nav-cart"]') + button = self.get_amazon_element("CART") except sel_exceptions.NoSuchElementException: log.info("Could not find cart button") current_page = self.driver.title @@ -1301,24 +1295,15 @@ def handle_cart(self): button = None while True: try: - button = self.driver.find_element_by_xpath( - '//*[@id="hlb-ptc-btn-native"] | //input[@name="proceedToRetailCheckout"]' - ) + button = self.get_amazon_element(key="PTC") break except sel_exceptions.NoSuchElementException: - try: - button = self.driver.find_element_by_xpath('//*[@id="hlb-ptc-btn"]') - break - except sel_exceptions.NoSuchElementException: - # also check for address buttons here? - if self.shipping_bypass(): - try: - button = self.driver.find_element_by_xpath( - join_xpaths(amazon_config["XPATH_ADDRESS"]) - ) - break - except sel_exceptions.NoSuchElementException: - pass + if self.shipping_bypass: + try: + button = self.get_amazon_element(key="ADDRESS_SELECT") + break + except sel_exceptions.NoSuchElementException: + pass if time.time() > timeout: log.info("couldn't find buttons to proceed to checkout") From 1278f930db26fcdfb6c409706204a662b07c667c Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 18:23:22 -0500 Subject: [PATCH 115/150] Update amazon.py switching some code over to the new methods --- stores/amazon.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index ec6d99b1..ca507c26 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -939,7 +939,7 @@ def remove_asin_list(self, asin): @debug def navigate_pages(self, test): title = self.driver.title - log.info(f"Navigating page title: '{title}'") + log.debug(f"Navigating page title: '{title}'") # see if this resolves blank page title issue? if title == "": timeout_seconds = DEFAULT_MAX_TIMEOUT @@ -1157,21 +1157,15 @@ def handle_shipping_page(self): page_name="choose-shipping", take_screenshot=self.take_screenshots, ) - try: - with self.wait_for_page_content_change(timeout=10): - element.click() - log.info("Clicked button.") + if self.do_button_click( + button=element, fail_text="Could not click ship to address button" + ): return True - except sel_exceptions.WebDriverException as e: - log.error("Could not click ship to address button") - log.error(e) - self.save_screenshot(page="shipping-select-error") - self.save_page_source(page="shipping-select-error") - else: - log.error("FairGame cannot find a button to click on the shipping page") - self.save_screenshot(page="shipping-select-error") - self.save_page_source(page="shipping-select-error") + # if we make it this far, it failed to click button + log.error("FairGame cannot find a button to click on the shipping page") + self.save_screenshot(page="shipping-select-error") + self.save_page_source(page="shipping-select-error") return False def get_amazon_element(self, key): @@ -1322,7 +1316,8 @@ def handle_cart(self): self.try_to_checkout = False else: log.info("Refreshing page to try again") - self.driver.refresh() + with self.wait_for_page_content_change(): + self.driver.refresh() self.checkout_retry += 1 return @@ -1334,15 +1329,14 @@ def handle_cart(self): page_name="ptc", take_screenshot=self.take_screenshots, ) - try: - with self.wait_for_page_content_change(): - self.do_button_click(button=button) - except sel_exceptions.WebDriverException: + if self.do_button_click(button=button): + return + else: log.error("Problem clicking Proceed to Checkout button.") log.info("Refreshing page to try again") with self.wait_for_page_content_change(): self.driver.refresh() - self.checkout_retry += 1 + self.checkout_retry += 1 @debug def handle_checkout(self, test): From 06a747147e01d54b4128ce7b93c4123883d12a5e Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 18:56:18 -0500 Subject: [PATCH 116/150] amazon.py xpath dictionary fix fix xpath dictionary - python :frowning: --- config/fairgame.conf | 19 +++++++++---------- stores/amazon.py | 6 +++--- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 32b8af36..0061e151 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -169,29 +169,28 @@ "Select a delivery address", "Ordine in preparazione", "Select a shipping address" - ] - "XPATHS": - { - "ADDRESS_SELECT":[ + ], + "XPATHS": { + "ADDRESS_SELECT": [ '//*[contains(@class,"ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium")]', '//input[@type="submit"]', '//*[@id="orderSummaryPrimaryActionBtn"]' ], - "PRIME_NO_THANKS":[ + "PRIME_NO_THANKS": [ '//*[contains(@class, "no-thanks-button")]', '//*[contains(@class, "prime-nothanks-button")]', '//*[contains(@class, "prime-no-button")]' ], - "CART":[ + "CART": [ '//*[@id="nav-cart-count"]' - ] - "PTC":[ + ], + "PTC": [ '//*[@id="hlb-ptc-btn-native"]', '//input[@name="proceedToRetailCheckout"]', '//*[@id="hlb-ptc-btn"]', '//*[@id="sc-buy-box-ptc-button"]' - ] - "ATC":[ + ], + "ATC": [ "//div[@id='aod-pinned-offer' or @id='aod-offer' or @id='olpOfferList']//input[@name='submit.addToCart']" ] } diff --git a/stores/amazon.py b/stores/amazon.py index ca507c26..a37707dd 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -89,7 +89,6 @@ DEFAULT_MAX_URL_FAIL = 5 amazon_config = {} -amazon_xpath = {} class Amazon: @@ -145,7 +144,6 @@ def __init__( from cli.cli import global_config amazon_config = global_config.get_amazon_config(encryption_pass) - amazon_xpath = amazon_config["XPATHS"] self.profile_path = global_config.get_browser_profile_path() try: @@ -1169,7 +1167,9 @@ def handle_shipping_page(self): return False def get_amazon_element(self, key): - return self.driver.find_element_by_xpath(join_xpaths(amazon_xpath[key])) + return self.driver.find_element_by_xpath( + join_xpaths(amazon_config["XPATHS"][key]) + ) # returns negative number if cart element does not exist, returns number if cart exists def get_cart_count(self): From 13726615b848831e465d510ffb6d5b17479e89be Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 26 Feb 2021 20:38:01 -0500 Subject: [PATCH 117/150] --Swapped quote styles in XPATh entries --- config/fairgame.conf | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 0061e151..c2a67e0d 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -172,23 +172,23 @@ ], "XPATHS": { "ADDRESS_SELECT": [ - '//*[contains(@class,"ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium")]', - '//input[@type="submit"]', - '//*[@id="orderSummaryPrimaryActionBtn"]' + "//*[contains(@class,'ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium')]", + "//input[@type='submit']", + "//*[@id='orderSummaryPrimaryActionBtn']" ], "PRIME_NO_THANKS": [ - '//*[contains(@class, "no-thanks-button")]', - '//*[contains(@class, "prime-nothanks-button")]', - '//*[contains(@class, "prime-no-button")]' + "//*[contains(@class, 'no-thanks-button')]", + "//*[contains(@class, 'prime-nothanks-button')]", + "//*[contains(@class, 'prime-no-button')]" ], "CART": [ - '//*[@id="nav-cart-count"]' + "//*[@id='nav-cart-count']" ], "PTC": [ - '//*[@id="hlb-ptc-btn-native"]', - '//input[@name="proceedToRetailCheckout"]', - '//*[@id="hlb-ptc-btn"]', - '//*[@id="sc-buy-box-ptc-button"]' + "//*[@id='hlb-ptc-btn-native']", + "//input[@name='proceedToRetailCheckout']", + "//*[@id='hlb-ptc-btn']", + "//*[@id='sc-buy-box-ptc-button']" ], "ATC": [ "//div[@id='aod-pinned-offer' or @id='aod-offer' or @id='olpOfferList']//input[@name='submit.addToCart']" From 3ec90ada8541ef196e142ba8bf6beb5eafb77367 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 20:58:37 -0500 Subject: [PATCH 118/150] Update amazon.py fixed atc_buttons (I think), was using wrong function. --- stores/amazon.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index a37707dd..5a6d82ad 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -665,7 +665,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) continue - atc_buttons: WebElement = self.get_amazon_element(key="ATC") + atc_buttons: WebElement = self.get_amazon_elements(key="ATC") # if not atc_buttons: # # Sanity check to see if we have a valid page, but no offers: # offer_count = WebDriverWait(self.driver, timeout=25).until( @@ -1171,6 +1171,11 @@ def get_amazon_element(self, key): join_xpaths(amazon_config["XPATHS"][key]) ) + def get_amazon_elements(self, key): + return self.driver.find_elements_by_xpath( + join_xpaths(amazon_config["XPATHS"][key]) + ) + # returns negative number if cart element does not exist, returns number if cart exists def get_cart_count(self): # check if cart number is on the page, if cart items = 0 From 5334632ba3fff64900f7ad59748a90c4db428c8c Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 22:42:05 -0500 Subject: [PATCH 119/150] amazon.py updates maybe it will click the choose address button? --- config/fairgame.conf | 5 ++++- stores/amazon.py | 24 ++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index c2a67e0d..0cb5a0fc 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -174,7 +174,10 @@ "ADDRESS_SELECT": [ "//*[contains(@class,'ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium')]", "//input[@type='submit']", - "//*[@id='orderSummaryPrimaryActionBtn']" + "//span[@id='orderSummaryPrimaryActionBtn']", + "//span[@id='orderSummaryPrimaryActionBtn-announce']", + "//span[@class='os-primary-action-button-text buy-button-line-height']", + "//input[@data-testid='Address_selectShipToThisAddress']" ], "PRIME_NO_THANKS": [ "//*[contains(@class, 'no-thanks-button')]", diff --git a/stores/amazon.py b/stores/amazon.py index 5a6d82ad..f46637f5 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -665,7 +665,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) continue - atc_buttons: WebElement = self.get_amazon_elements(key="ATC") + atc_buttons = self.get_amazon_elements(key="ATC") # if not atc_buttons: # # Sanity check to see if we have a valid page, but no offers: # offer_count = WebDriverWait(self.driver, timeout=25).until( @@ -1033,7 +1033,7 @@ def navigate_pages(self, test): button=element, clicking_text="FairGame thinks it is seeing a Prime Offer, attempting to click No Thanks", fail_text="FairGame could not click No Thanks button", - debug=True, + log_debug=True, ): return # see if a use this address (or similar) button is on page (based on known xpaths). Only check if @@ -1238,7 +1238,7 @@ def do_button_click( clicking_text="Clicking button", clicked_text="Button clicked", fail_text="Could not click button", - debug=False, + log_debug=False, ): try: with self.wait_for_page_content_change(): @@ -1247,7 +1247,7 @@ def do_button_click( log.info(clicked_text) return True except sel_exceptions.WebDriverException as e: - if debug: + if log_debug: log.debug(fail_text) log.debug(e) else: @@ -1352,17 +1352,21 @@ def handle_checkout(self, test): try: button = self.driver.find_element_by_xpath(self.button_xpaths[0]) except sel_exceptions.NoSuchElementException: - pass - self.button_xpaths.append(self.button_xpaths.pop(0)) + if self.shipping_bypass: + try: + button = self.get_amazon_element(key="ADDRESS_SELECT") + except sel_exceptions.NoSuchElementException: + pass + self.button_xpaths.append(self.button_xpaths.pop(0)) if button: if button.is_enabled() and button.is_displayed(): break if time.time() > timeout: - log.error("couldn't find buttons to proceed to checkout") - self.save_page_source("ptc-error") + log.error("couldn't find button to place order") + self.save_page_source("pyo-error") self.send_notification( - "Error in checkout. Please check browser window.", - "ptc-error", + "Error in placing order. Please check browser window.", + "pyo-error", self.take_screenshots, ) log.info("Refreshing page to try again") From c4fc6f2155acf877b294a13493b9f34c20d2dfa0 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 22:52:01 -0500 Subject: [PATCH 120/150] Update fairgame.conf clean up Address_Select xpaths --- config/fairgame.conf | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 0cb5a0fc..6ad3e78f 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -172,12 +172,9 @@ ], "XPATHS": { "ADDRESS_SELECT": [ - "//*[contains(@class,'ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium')]", - "//input[@type='submit']", - "//span[@id='orderSummaryPrimaryActionBtn']", - "//span[@id='orderSummaryPrimaryActionBtn-announce']", - "//span[@class='os-primary-action-button-text buy-button-line-height']", - "//input[@data-testid='Address_selectShipToThisAddress']" + "//*[contains(@class,'ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium')]", + "//span[@id='orderSummaryPrimaryActionBtn-announce']", + "//input[@data-testid='Address_selectShipToThisAddress']" ], "PRIME_NO_THANKS": [ "//*[contains(@class, 'no-thanks-button')]", From b30cdda52f372187bd2e1aed800593d37b624582 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 22:56:42 -0500 Subject: [PATCH 121/150] Update fairgame.conf I guess one of the address xpaths doesn't work? --- config/fairgame.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 6ad3e78f..2471f9d2 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -173,7 +173,7 @@ "XPATHS": { "ADDRESS_SELECT": [ "//*[contains(@class,'ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium')]", - "//span[@id='orderSummaryPrimaryActionBtn-announce']", + "//input[@aria-labelledby='orderSummaryPrimaryActionBtn-announce']", "//input[@data-testid='Address_selectShipToThisAddress']" ], "PRIME_NO_THANKS": [ From ec3f2e8f8d72ab0f4a2a967d2eb9f8bb0311d1dc Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 26 Feb 2021 23:37:09 -0500 Subject: [PATCH 122/150] Update amazon.py short-circuit ptc timeout if nothing in cart. --- stores/amazon.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index f46637f5..2b245411 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1303,6 +1303,10 @@ def handle_cart(self): break except sel_exceptions.NoSuchElementException: pass + if self.get_cart_count() == 0: + log.info("You have no items in cart. Going back to stock check.") + self.try_to_checkout = False + break if time.time() > timeout: log.info("couldn't find buttons to proceed to checkout") @@ -1312,18 +1316,18 @@ def handle_cart(self): "ptc-error", self.take_screenshots, ) - if self.get_cart_count() == 0: - log.info("It appears this is because you have no items in cart.") - log.info( - "It is likely that the product went out of stock before you could checkout" - ) - log.info("Going back to stock check.") - self.try_to_checkout = False - else: - log.info("Refreshing page to try again") - with self.wait_for_page_content_change(): - self.driver.refresh() - self.checkout_retry += 1 + # if self.get_cart_count() == 0: + # log.info("It appears this is because you have no items in cart.") + # log.info( + # "It is likely that the product went out of stock before you could checkout" + # ) + # log.info("Going back to stock check.") + # self.try_to_checkout = False + # else: + log.info("Refreshing page to try again") + with self.wait_for_page_content_change(): + self.driver.refresh() + self.checkout_retry += 1 return if button: From 54db42e74e7d6910fc7233d57c716026edc3510f Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Sat, 27 Feb 2021 00:00:09 -0500 Subject: [PATCH 123/150] amazon.py updates some fixes if/when it returns to homepage --- config/fairgame.conf | 6 ++++- stores/amazon.py | 58 ++++++++++++++++++-------------------------- utils/version.py | 2 +- 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 2471f9d2..8ffa92f3 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -71,7 +71,8 @@ "Amazon.de: Günstige Preise für Elektronik & Foto, Filme, Musik, Bücher, Games, Spielzeug & mehr", "Amazon.fr : livres, DVD, jeux vidéo, musique, high-tech, informatique, jouets, vêtements, chaussures, sport, bricolage, maison, beauté, puériculture, épicerie et plus encore !", "Amazon.it: elettronica, libri, musica, fashion, videogiochi, DVD e tanto altro", - "Amazon.nl: Groot aanbod, kleine prijzen in o.a. Elektronica, boeken, sport en meer" + "Amazon.nl: Groot aanbod, kleine prijzen in o.a. Elektronica, boeken, sport en meer", + "Your AmazonSmile" ], "SHOPPING_CART_TITLES": [ "Amazon.com Shopping Cart", @@ -184,6 +185,9 @@ "CART": [ "//*[@id='nav-cart-count']" ], + "CART_BUTTON":[ + "//*[@id='nav-cart']" + ] "PTC": [ "//*[@id='hlb-ptc-btn-native']", "//input[@name='proceedToRetailCheckout']", diff --git a/stores/amazon.py b/stores/amazon.py index 2b245411..ed712ff6 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -883,44 +883,32 @@ def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): f = f"{AMAZON_URLS['ATC_URL']}?OfferListingId.1={offering_id}&Quantity.1=1" atc_attempts = 0 while atc_attempts < max_atc_retries: - try: - with self.wait_for_page_content_change(timeout=5): + with self.wait_for_page_content_change(timeout=5): + try: self.driver.get(f) - xpath = "//input[@value='add' and @name='add']" - if wait_for_element_by_xpath(self.driver, xpath): - try: - with self.wait_for_page_content_change(timeout=15): - continue_btn = WebDriverWait( - self.driver, timeout=5 - ).until(EC.element_to_be_clickable((By.XPATH, xpath))) - continue_btn.click() - except sel_exceptions.NoSuchElementException: - log.error("Continue button not present on page") - except ( - sel_exceptions.ElementNotInteractableException, - sel_exceptions.ElementClickInterceptedException, - sel_exceptions.ElementNotVisibleException, - sel_exceptions.TimeoutException, - ) as e: - log.error(f"Could not click button due to {e}") - self.save_screenshot(page="atc-continue-button`") - self.save_page_source(page="atc-continue-button`") - except sel_exceptions.WebDriverException as e: - log.error("Selenium Error") - log.error(e) - self.save_screenshot(page="atc-error") - self.save_page_source(page="atc-error") - else: - log.error("Continue button not present on page") - - # verify cart is non-zero + except sel_exceptions.TimeoutException: + log.error("Failed to get page") + atc_attempts += 1 + continue + xpath = "//input[@value='add' and @name='add']" + if wait_for_element_by_xpath(self.driver, xpath): + continue_btn = None + try: + continue_btn = WebDriverWait(self.driver, timeout=5).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + except sel_exceptions.TimeoutException: + log.error("No continue button found") + if continue_btn: + if self.do_button_click( + button=continue_btn, fail_text="Could not click continue button" + ): if self.get_cart_count() != 0: return True else: - atc_attempts = atc_attempts + 1 - except sel_exceptions.TimeoutException: - log.error("Could not load webpage within timeout period") - atc_attempts += 1 + log.info("Nothing added to cart, trying again") + + atc_attempts = atc_attempts + 1 return False # search lists of asin lists, and remove the first list that matches provided asin @@ -1260,7 +1248,7 @@ def handle_home_page(self): log.info("On home page, trying to get back to checkout") button = None try: - button = self.get_amazon_element("CART") + button = self.get_amazon_element("CART_BUTTON") except sel_exceptions.NoSuchElementException: log.info("Could not find cart button") current_page = self.driver.title diff --git a/utils/version.py b/utils/version.py index fe69b50e..a0ef7edd 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.1.dev2" +__VERSION = "0.6.1.dev3" version = Version(__VERSION) From b842ab4b9f97025bc8c20043e0cd5935e24401ee Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Sat, 27 Feb 2021 00:49:32 -0500 Subject: [PATCH 124/150] Update amazon.py minor changes, probably does nothing. --- stores/amazon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index ed712ff6..d8cfa06f 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -909,6 +909,7 @@ def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): log.info("Nothing added to cart, trying again") atc_attempts = atc_attempts + 1 + log.error("reached maximum ATC attempts, returning to stock check") return False # search lists of asin lists, and remove the first list that matches provided asin @@ -1376,8 +1377,7 @@ def handle_checkout(self, test): self.asin_list = [] else: log.info(f"Clicking Button {button.text} to place order") - button.click() - self.wait_for_page_change(page_title=previous_title) + self.do_button_click(button=button) @debug def handle_order_complete(self): From 329241e1653e74d399d9a7412b3950cabb1615ea Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Sat, 27 Feb 2021 11:35:34 -0500 Subject: [PATCH 125/150] Update README.md Clarifying purchasing loop --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6b985c5..6849c151 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ ASIN?*](https://www.datafeedwatch.com/blog/amazon-asin-number-what-is-it-and-how * `asin_groups` indicates the number of ASIN groups you want to use. * `asin_list_x` list of ASINs for products you want to purchase. You must locate these for the products you want, use the links above to get started. - * The first time an item from list "x" is in stock and under its associated reserve, it will purchase it. + * The first time an item from list "x" is in stock and under its associated reserve, it will purchase it. FairGame will continue to loop through the other lists until it purchases one item from each (unless the `--single-shot` option is enabled, in which case it stops after the first purchase). * If the purchase is successful, the bot will not buy anything else from list "x". * Use sequential numbers for x, starting from 1. x can be any integer from 1 to 18,446,744,073,709,551,616 * `reserve_min_x` set a minimum limit to consider for purchasing an item. If a seller has a listing for a 700 dollar From ecad507cf963f7ce0fba85ae5e6330c23717a9c0 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sat, 27 Feb 2021 20:50:42 -0500 Subject: [PATCH 126/150] --breakfix for the footer loaded identifier on amazon business accounts. Added //div[@id='navFooter'] --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index d8cfa06f..70bc03f7 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -519,7 +519,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): self.driver, timeout=DEFAULT_MAX_TIMEOUT ).until( lambda d: d.find_elements_by_xpath( - "//div[@class='nav-footer-line'] | //img[@alt='Dogs of Amazon']" + "//div[@class='nav-footer-line'] | //div[@id='navFooter'] | //img[@alt='Dogs of Amazon']" ) ) if footer and footer[0].tag_name == "img": From 8eb76be535436e8d45a82bca45d8b8318df343c5 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Sun, 28 Feb 2021 14:18:28 -0500 Subject: [PATCH 127/150] Update README.md added some tl;dr bullet points at start of usage. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 6849c151..c3971297 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,13 @@ Then save and close the file. ### Amazon +TL;DR Notes: +* By default, bot will only purchase new items with free shipping. +* Running the FairGame with the `_Amazon.bat` is easiest. You should change the name of the `_Amazon.bat` file though, so it does not overwrite any changes you made (adding flags, removing `--test`, etc.) +* Make a copy of the `amazon_config.template_json` file, and rename it to `amazon_config.json`. Modify it as _you_ see fit, with ASINs and min/max reserve prices as you think they should be set. +* **DO NOT ADD STUFF TO YOUR CART WHILE THE BOT IS RUNNING - IF IT ATTEMPTS TO CHECKOUT, AND THERE ARE ITEMS IN THE CART, BUT YOUR TARGET ITEM DOES NOT ADD TO CART CORRECTLY, IT WILL PURCHASE WHATEVER WAS IN THE CART AND THINK THAT IT PURCHASED YOUR TARGET ITEM.** +* **EVEN THOUGH THIS IS "TL;DR" YOU STILL NEED TO READ THE WHOLE THING!** + The following flags are specific to the Amazon scripts. They the `[OPTIONS]` to be passed on the command-line to control the behavior of Amazon scanning and purchasing. These can be added at the command line or added to a batch file/shell script (see `_Amazon.bat` in the root folder of the project). **NOTE:** `--test` flag has been added to `_Amazon.bat` From e3cc99dc4ce96086163a93fb29e731e3312ac161 Mon Sep 17 00:00:00 2001 From: cy1110 Date: Sun, 28 Feb 2021 21:32:53 -0500 Subject: [PATCH 128/150] Add try-catch to prevent crash when TimeoutException is thrown by wait --- stores/amazon.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 70bc03f7..111ecc42 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1497,10 +1497,18 @@ def wait_for_page_content_change(self, timeout=30): """Utility to help manage selenium waiting for a page to load after an action, like a click""" old_page = self.driver.find_element_by_tag_name("html") yield - WebDriverWait(self.driver, timeout).until(EC.staleness_of(old_page)) - WebDriverWait(self.driver, timeout).until( - EC.presence_of_element_located((By.XPATH, "//title")) - ) + try: + WebDriverWait(self.driver, timeout).until(EC.staleness_of(old_page)) + WebDriverWait(self.driver, timeout).until( + EC.presence_of_element_located((By.XPATH, "//title")) + ) + except sel_exceptions.TimeoutException: + log.info("Timed out reloading page, trying to continue anyway") + pass + except Exception as e: + log.error(f"Trying to recover from error: {e}") + pass + return None def wait_for_page_change(self, page_title, timeout=3): time_to_end = self.get_timeout(timeout=timeout) From 83d67e7ddd679caefc46cbfac8808fe2d0c71f7d Mon Sep 17 00:00:00 2001 From: cy1110 Date: Sun, 28 Feb 2021 22:47:58 -0500 Subject: [PATCH 129/150] Reduce delay to 10s for refresh timeout, stop blocking for 5 minutes if stuck on home page --- stores/amazon.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 111ecc42..6985bbec 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1100,7 +1100,7 @@ def navigate_pages(self, test): ): return else: - with self.wait_for_page_content_change(): + with self.wait_for_page_content_change(timeout=10): self.driver.refresh() return @@ -1110,7 +1110,7 @@ def navigate_pages(self, test): ) self.save_page_source(page="unknown") self.save_screenshot(page="unknown") - with self.wait_for_page_content_change(): + with self.wait_for_page_content_change(timeout=10): self.driver.refresh() return @@ -1248,10 +1248,15 @@ def do_button_click( def handle_home_page(self): log.info("On home page, trying to get back to checkout") button = None - try: - button = self.get_amazon_element("CART_BUTTON") - except sel_exceptions.NoSuchElementException: - log.info("Could not find cart button") + tries = 0 + maxTries = 10 + while not button and tries < maxTries: + try: + button = self.get_amazon_element("CART_BUTTON") + except sel_exceptions.NoSuchElementException: + log.info("Could not find cart button") + tries += 1 + sleep(0.5) current_page = self.driver.title if button: if self.do_button_click(button=button): From 9aa75a851d7857509beb6a511fe3b74ad44640f6 Mon Sep 17 00:00:00 2001 From: cy1110 Date: Sun, 28 Feb 2021 22:58:49 -0500 Subject: [PATCH 130/150] Reduce delay to 5s for refresh timeout, improve homepage stuck log format --- stores/amazon.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 6985bbec..e96ade40 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1100,7 +1100,7 @@ def navigate_pages(self, test): ): return else: - with self.wait_for_page_content_change(timeout=10): + with self.wait_for_page_content_change(timeout=5): self.driver.refresh() return @@ -1110,7 +1110,7 @@ def navigate_pages(self, test): ) self.save_page_source(page="unknown") self.save_screenshot(page="unknown") - with self.wait_for_page_content_change(timeout=10): + with self.wait_for_page_content_change(timeout=5): self.driver.refresh() return @@ -1254,13 +1254,17 @@ def handle_home_page(self): try: button = self.get_amazon_element("CART_BUTTON") except sel_exceptions.NoSuchElementException: - log.info("Could not find cart button") + pass tries += 1 sleep(0.5) current_page = self.driver.title if button: if self.do_button_click(button=button): return + else: + log.info("Failed to click on cart button") + else: + log.info("Could not find cart button after " + str(maxTries) + " tries") # no button found or could not interact with the button self.send_notification( From 9280de388ece2eca22e8e5568dd482bbf8f96f42 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Sun, 28 Feb 2021 23:09:36 -0500 Subject: [PATCH 131/150] --Correctly interpret --checkshipping for AOD. Possibly broke offers, but impossible to test. --Added new Free Delivery message --Added alternate shipping cost parser if primary method fails --Updated selector to only select offer nodes if they contain an add to cart button (pinned offers don't always) --Added typing for List[WebElements] --- config/fairgame.conf | 3 +- stores/amazon.py | 138 +++++++++++++++++++++++++++---------------- 2 files changed, 90 insertions(+), 51 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 8ffa92f3..f112bd1e 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -164,7 +164,8 @@ "GRATIS-LIEFERUNG", "PRIME FREE DELIVERY", "DELIVERY AT NO EXTRA COST FOR PRIME MEMBERS", - "GRATIS LIEFERUNG FÜR PRIME-MITGLIEDER" + "GRATIS LIEFERUNG FÜR PRIME-MITGLIEDER", + "FREE DELIVERY:" ], "ADDRESS_SELECT": [ "Select a delivery address", diff --git a/stores/amazon.py b/stores/amazon.py index 70bc03f7..02fc52c9 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -26,6 +26,7 @@ from contextlib import contextmanager from datetime import datetime from enum import Enum +from typing import List import psutil from amazoncaptcha import AmazonCaptcha @@ -515,7 +516,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): # Sanity check to see if we have any offers try: # Wait for the page to load before determining what's in it by looking for the footer - footer: WebElement = WebDriverWait( + footer: List[WebElement] = WebDriverWait( self.driver, timeout=DEFAULT_MAX_TIMEOUT ).until( lambda d: d.find_elements_by_xpath( @@ -719,61 +720,68 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): return False shipping = [] shipping_prices = [] - if self.checkshipping: - timeout = self.get_timeout() - while True: - if not flyout_mode: - shipping = self.driver.find_elements_by_xpath( - '//*[@class="a-color-secondary"]' - ) - if shipping: - # Convert to prices just in case - for idx, shipping_node in enumerate(shipping): - log.debug(f"Processing shipping node {idx}") - if self.checkshipping: - if amazon_config["SHIPPING_ONLY_IF"] in shipping_node.text: - shipping_prices.append(parse_price("0")) - else: - shipping_prices.append(parse_price(shipping_node.text)) - else: + + timeout = self.get_timeout() + while True: + if not flyout_mode: + shipping = self.driver.find_elements_by_xpath( + '//*[@class="a-color-secondary"]' + ) + if shipping: + # Convert to prices just in case + for idx, shipping_node in enumerate(shipping): + log.debug(f"Processing shipping node {idx}") + if self.checkshipping: + if amazon_config["SHIPPING_ONLY_IF"] in shipping_node.text: shipping_prices.append(parse_price("0")) - else: - # Check for offers - offers = self.driver.find_elements_by_xpath( - "//div[@id='aod-pinned-offer' or @id='aod-offer']" + else: + shipping_prices.append(parse_price(shipping_node.text)) + else: + shipping_prices.append(parse_price("0")) + else: + # Check for offers + # offer_xpath = "//div[@id='aod-pinned-offer' or @id='aod-offer']" + offer_xpath = ( + "//div[@id='aod-offer' and .//input[@name='submit.addToCart']] | " + "//div[@id='aod-pinned-offer' and .//input[@name='submit.addToCart']]" + ) + offers = self.driver.find_elements_by_xpath(offer_xpath) + for idx, offer in enumerate(offers): + tree = html.fromstring(offer.get_attribute("innerHTML")) + shipping_prices.append( + get_shipping_costs(tree, amazon_config["FREE_SHIPPING"]) ) - for idx, offer in enumerate(offers): - tree = html.fromstring(offer.get_attribute("innerHTML")) - shipping_prices.append( - get_shipping_costs(tree, amazon_config["FREE_SHIPPING"]) - ) - if shipping_prices: - break + if shipping_prices: + break - if time.time() > timeout: - log.info(f"failed to load shipping for {asin}, going to next ASIN") - return False + if time.time() > timeout: + log.info(f"failed to load shipping for {asin}, going to next ASIN") + return False in_stock = False for shipping_price in shipping_prices: log.debug(f"\tShipping Price: {shipping_price}") for idx, atc_button in enumerate(atc_buttons): + # If the user has specified that they only want free items, we can skip any items + # that have any shipping cost and early out + if not self.checkshipping and shipping_prices[idx].amount_float > 0.00: + continue + # Condition check first, using the button to find the form that will divulge the item's condition if flyout_mode: - condition: WebElement = atc_button.find_elements_by_xpath( + condition: List[WebElement] = atc_button.find_elements_by_xpath( "./ancestor::form[@method='post']" ) - if condition: atc_form_action = condition[0].get_attribute("action") - item_condition = get_item_condition(atc_form_action) + seller_item_condition = get_item_condition(atc_form_action) # Lower condition value imply newer - if item_condition.value > self.condition.value: + if seller_item_condition.value > self.condition.value: # Item is below our standards, so skip it log.debug( f"Skipping item because its condition is below the requested level: " - f"{item_condition} is below {self.condition}" + f"{seller_item_condition} is below {self.condition}" ) continue @@ -785,19 +793,13 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): except IndexError: log.debug("Price index error") return False - try: - if self.checkshipping: - ship_price = shipping_prices[idx] - else: - ship_price = parse_price("0") - except IndexError: - log.debug("shipping index error") - return False + # Include the price, even if it's zero for comparison + ship_price = shipping_prices[idx] ship_float = ship_price.amount price_float = price.amount if price_float is None: return False - if ship_float is None or not self.checkshipping: + if ship_float is None: ship_float = 0 if ( @@ -808,6 +810,10 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): or math.isclose((price_float + ship_float), reserve_min, abs_tol=0.01) ): log.info("Item in stock and in reserve range!") + log.info(f"{price_float} + {ship_float} shipping <= {reserve_max}") + log.debug( + f"{reserve_min} <= {price_float} + {ship_float} shipping <= {reserve_max}" + ) log.info("Adding to cart") # Get the offering ID offering_id_elements = atc_button.find_elements_by_xpath( @@ -891,8 +897,8 @@ def attempt_atc(self, offering_id, max_atc_retries=DEFAULT_MAX_ATC_TRIES): atc_attempts += 1 continue xpath = "//input[@value='add' and @name='add']" + continue_btn = None if wait_for_element_by_xpath(self.driver, xpath): - continue_btn = None try: continue_btn = WebDriverWait(self.driver, timeout=5).until( EC.element_to_be_clickable((By.XPATH, xpath)) @@ -1707,18 +1713,50 @@ def get_timestamp_filename(name, extension): return name + "_" + date + "." + extension -def get_shipping_costs(tree, free_shipping_string) -> Price: +def get_shipping_costs(tree, free_shipping_string): + # This version expects to find the shipping pricing within a div with the explicit ID 'delivery-message' + shipping_xpath = ".//div[@id='delivery-message']" + shipping_nodes = tree.xpath(shipping_xpath) + count = len(shipping_nodes) + if count > 0: + # Get the text out of the div and evaluate it + shipping_node = shipping_nodes[0] + if shipping_node.text: + shipping_span_text = shipping_node.text.strip() + if any( + shipping_span_text.upper() in free_message + for free_message in amazon_config["FREE_SHIPPING"] + ): + # We found some version of "free" inside the span.. but this relies on a match + log.info( + f"Assuming free shipping based on this message: '{shipping_span_text}'" + ) + return FREE_SHIPPING_PRICE + else: + # will it parse? + shipping_cost: Price = parse_price(shipping_span_text) + if shipping_cost.currency is not None: + log.debug( + f"Found parseable price with currency symbol: {shipping_cost.currency}" + ) + return shipping_cost + # Try the alternative method... + return get_alt_shipping_costs(tree, free_shipping_string) + + +def get_alt_shipping_costs(tree, free_shipping_string) -> Price: # Assume Free Shipping and change otherwise # Shipping collection xpath: # .//div[starts-with(@id, 'aod-bottlingDepositFee-')]/following-sibling::span - shipping_nodes = tree.xpath( + shipping_xpath = ( ".//div[starts-with(@id, 'aod-bottlingDepositFee-')]/following-sibling::*[1]" ) + shipping_nodes = tree.xpath(shipping_xpath) count = len(shipping_nodes) log.debug(f"Found {count} shipping nodes.") if count == 0: - log.warning("No shipping nodes found. Assuming zero.") + log.warning("No shipping nodes (standard or alt) found. Assuming zero.") return FREE_SHIPPING_PRICE elif count > 1: log.warning("Found multiple shipping nodes. Using the first.") From 62c4f8c83cbb6d763e7f4583668ca9343fda16ec Mon Sep 17 00:00:00 2001 From: cy1110 Date: Sun, 28 Feb 2021 23:23:43 -0500 Subject: [PATCH 132/150] Fix typo --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index e96ade40..1f902fa0 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1256,7 +1256,7 @@ def handle_home_page(self): except sel_exceptions.NoSuchElementException: pass tries += 1 - sleep(0.5) + time.sleep(0.5) current_page = self.driver.title if button: if self.do_button_click(button=button): From d793c0d5282fff544561e4ce876ef80fc94d3d60 Mon Sep 17 00:00:00 2001 From: cy1110 Date: Sun, 28 Feb 2021 23:28:49 -0500 Subject: [PATCH 133/150] Set 10s reload to default --- stores/amazon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stores/amazon.py b/stores/amazon.py index 1f902fa0..b3f690f4 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1100,7 +1100,7 @@ def navigate_pages(self, test): ): return else: - with self.wait_for_page_content_change(timeout=5): + with self.wait_for_page_content_change(): self.driver.refresh() return @@ -1110,7 +1110,7 @@ def navigate_pages(self, test): ) self.save_page_source(page="unknown") self.save_screenshot(page="unknown") - with self.wait_for_page_content_change(timeout=5): + with self.wait_for_page_content_change(): self.driver.refresh() return @@ -1502,7 +1502,7 @@ def save_page_source(self, page): f.write(page_source) @contextmanager - def wait_for_page_content_change(self, timeout=30): + def wait_for_page_content_change(self, timeout=5): """Utility to help manage selenium waiting for a page to load after an action, like a click""" old_page = self.driver.find_element_by_tag_name("html") yield From 50859e88aa84d90d84653eb9ce5d54fb027420d0 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Mon, 1 Mar 2021 11:27:54 -0500 Subject: [PATCH 134/150] --Updated version for flag fixing feature --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index a0ef7edd..4119b850 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.1.dev3" +__VERSION = "0.6.1.dev4" version = Version(__VERSION) From 693bc9d4e03991ebccb57e10e6ebf2047c909d43 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Wed, 3 Mar 2021 13:01:07 -0500 Subject: [PATCH 135/150] --Updated version for flag fixing feature merge --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 4119b850..49546e01 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.1.dev4" +__VERSION = "0.6.1.dev5" version = Version(__VERSION) From b2f869259d37fa8df7c9a0a10390b30f89011f0a Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 4 Mar 2021 20:46:31 -0500 Subject: [PATCH 136/150] --Added missing comma for validation --- config/fairgame.conf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index f112bd1e..9eb8e0b6 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -174,9 +174,9 @@ ], "XPATHS": { "ADDRESS_SELECT": [ - "//*[contains(@class,'ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium')]", - "//input[@aria-labelledby='orderSummaryPrimaryActionBtn-announce']", - "//input[@data-testid='Address_selectShipToThisAddress']" + "//*[contains(@class,'ship-to-this-address a-button a-button-primary a-button-span12 a-spacing-medium')]", + "//input[@aria-labelledby='orderSummaryPrimaryActionBtn-announce']", + "//input[@data-testid='Address_selectShipToThisAddress']" ], "PRIME_NO_THANKS": [ "//*[contains(@class, 'no-thanks-button')]", @@ -186,9 +186,9 @@ "CART": [ "//*[@id='nav-cart-count']" ], - "CART_BUTTON":[ + "CART_BUTTON": [ "//*[@id='nav-cart']" - ] + ], "PTC": [ "//*[@id='hlb-ptc-btn-native']", "//input[@name='proceedToRetailCheckout']", From 3ebedb28f32cd0d2b592e1bdf2984847e78dc1a5 Mon Sep 17 00:00:00 2001 From: Andrew So Date: Tue, 9 Mar 2021 02:20:33 +0000 Subject: [PATCH 137/150] dynamically find scrypt cost factor based off of system memory --- utils/encryption.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/utils/encryption.py b/utils/encryption.py index 25d85c1e..046d296a 100644 --- a/utils/encryption.py +++ b/utils/encryption.py @@ -17,33 +17,25 @@ # The author may be contacted through the project's GitHub, at: # https://github.com/Hari-Nagarajan/fairgame +import getpass as getpass +import stdiomask import json -import platform +import math +import os from base64 import b64encode, b64decode - -import stdiomask from Crypto.Cipher import ChaCha20_Poly1305 -from Crypto.Protocol.KDF import scrypt from Crypto.Random import get_random_bytes +from Crypto.Protocol.KDF import scrypt +from psutil import virtual_memory from utils.logger import log -if platform.machine()[:3] == "arm": - # Not great, but workable way to detect Raspberry Pi - log.info( - "Using reduced CPU and Memory cost parameter for encryption for your platform." - ) - CPU_MEM_COST = 10 -else: - CPU_MEM_COST = 20 - def encrypt(pt, password): """Encryption function to securely store user credentials, uses ChaCha_Poly1305 with a user defined SCrypt key.""" salt = get_random_bytes(32) - - key = scrypt(password, salt, key_len=32, N=2 ** CPU_MEM_COST, r=8, p=1) + key = scrypt(password, salt, key_len=32, N=get_scrypt_cost_factor(), r=8, p=1) nonce = get_random_bytes(12) cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) ct, tag = cipher.encrypt_and_digest(pt) @@ -60,9 +52,8 @@ def decrypt(ct, password): b64Ct = json.loads(ct) json_k = ["nonce", "salt", "ct", "tag"] json_v = {k: b64decode(b64Ct[k]) for k in json_k} - key = scrypt( - password, json_v["salt"], key_len=32, N=2 ** CPU_MEM_COST, r=8, p=1 + password, json_v["salt"], key_len=32, N=get_scrypt_cost_factor(), r=8, p=1 ) cipher = ChaCha20_Poly1305.new(key=key, nonce=json_v["nonce"]) ptData = cipher.decrypt_and_verify(json_v["ct"], json_v["tag"]) @@ -121,6 +112,15 @@ def load_encrypted_config(config_path, encrypted_pass=None): ) +def get_scrypt_cost_factor(mem_percentage=0.5): + # Returns scrypt cost factor 'N' param based off of system memory + # Max value is 2 ** 20 + mem = math.floor(virtual_memory().total * mem_percentage / 1024) + # Value must be a power of 2 + exponent = math.floor(math.log(mem, 2)) + return min(2 ** 20, 2 ** exponent) + + # def main(): # # password = getpass.getpass(prompt="Password: ") From cac2515221511f25a3acfa748c3e0db16c8edd44 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 11 Mar 2021 10:00:00 -0500 Subject: [PATCH 138/150] --Added Turkish page title --- config/fairgame.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 9eb8e0b6..302fed8b 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -112,7 +112,8 @@ "Pagamento Amazon.it", "Ordine in preparazione", "Amazon.sg Checkout", - "A preparar o seu pedido" + "A preparar o seu pedido", + "Amazon.com.tr Alışverişi Tamamla" ], "ORDER_COMPLETE_TITLES": [ "Amazon.com Thanks You", From 9ee407c3da9520b01b4d15a3ee5aa9f9b4e82ada Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 11 Mar 2021 10:14:02 -0500 Subject: [PATCH 139/150] --Added Italian free shipping message --- config/fairgame.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 302fed8b..c2759240 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -166,7 +166,8 @@ "PRIME FREE DELIVERY", "DELIVERY AT NO EXTRA COST FOR PRIME MEMBERS", "GRATIS LIEFERUNG FÜR PRIME-MITGLIEDER", - "FREE DELIVERY:" + "FREE DELIVERY:", + "SPEDIZIONE SENZA COSTI AGGIUNTIVI CON PRIME" ], "ADDRESS_SELECT": [ "Select a delivery address", From c4036f0fbaf27ab9aa04b6d82a952a9845924931 Mon Sep 17 00:00:00 2001 From: unapproachable Date: Thu, 11 Mar 2021 10:42:08 -0500 Subject: [PATCH 140/150] --Additional Turkish titles (from user Mustafaul) --- config/fairgame.conf | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index c2759240..41311f33 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -72,7 +72,8 @@ "Amazon.fr : livres, DVD, jeux vidéo, musique, high-tech, informatique, jouets, vêtements, chaussures, sport, bricolage, maison, beauté, puériculture, épicerie et plus encore !", "Amazon.it: elettronica, libri, musica, fashion, videogiochi, DVD e tanto altro", "Amazon.nl: Groot aanbod, kleine prijzen in o.a. Elektronica, boeken, sport en meer", - "Your AmazonSmile" + "Your AmazonSmile", + "Amazon.com.tr Elektronik, bilgisayar, akıllı telefon, kitap, oyuncak, yapı market, ev, mutfak, oyun konsolları ürünleri ve daha fazlası için internet alışveriş sitesi" ], "SHOPPING_CART_TITLES": [ "Amazon.com Shopping Cart", @@ -86,7 +87,8 @@ "Carrello Amazon.it", "AmazonSmile Shopping Cart", "AmazonSmile Shopping Basket", - "Amazon.nl-winkelwagen" + "Amazon.nl-winkelwagen", + "Amazon.com.tr Alışveriş Sepeti" ], "CHECKOUT_TITLES": [ "Amazon.com Checkout", @@ -128,7 +130,8 @@ "Hartelijk dank", "Thank You", "Amazon.de Vielen Dank", - "Obrigado, o seu pedido foi efetuado" + "Obrigado, o seu pedido foi efetuado", + "Teşekkür Ederiz" ], "BUSINESS_PO_TITLES": [ "Business order information" From 7c022a2d9ac405c31cc9bec66006501b28ad3b99 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Sat, 13 Mar 2021 16:15:12 -0500 Subject: [PATCH 141/150] Update fairgame.conf added amazon.pl titles based on #540 --- config/fairgame.conf | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 41311f33..08c770ba 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -45,7 +45,8 @@ "Bonjour, Identifiez-vous", "Ciao, Accedi", "Hallo, Anmelden", - "Hallo, Inloggen" + "Hallo, Inloggen", + "Witamy, Zaloguj się" ], "SIGN_IN_TITLES": [ "Amazon Sign In", @@ -54,7 +55,8 @@ "Iniciar sesión en Amazon", "Connexion Amazon", "Amazon Accedi", - "Inloggen bij Amazon" + "Inloggen bij Amazon", + "Amazon Zaloguj się" ], "CAPTCHA_PAGE_TITLES": [ "Robot Check", @@ -73,7 +75,8 @@ "Amazon.it: elettronica, libri, musica, fashion, videogiochi, DVD e tanto altro", "Amazon.nl: Groot aanbod, kleine prijzen in o.a. Elektronica, boeken, sport en meer", "Your AmazonSmile", - "Amazon.com.tr Elektronik, bilgisayar, akıllı telefon, kitap, oyuncak, yapı market, ev, mutfak, oyun konsolları ürünleri ve daha fazlası için internet alışveriş sitesi" + "Amazon.com.tr Elektronik, bilgisayar, akıllı telefon, kitap, oyuncak, yapı market, ev, mutfak, oyun konsolları ürünleri ve daha fazlası için internet alışveriş sitesi", + "Amazon.pl: zakupy internetowe elektroniki, odzieży, komputerów, książek, płyt DVD i innych" ], "SHOPPING_CART_TITLES": [ "Amazon.com Shopping Cart", @@ -88,7 +91,8 @@ "AmazonSmile Shopping Cart", "AmazonSmile Shopping Basket", "Amazon.nl-winkelwagen", - "Amazon.com.tr Alışveriş Sepeti" + "Amazon.com.tr Alışveriş Sepeti", + "Amazon.pl Koszyk" ], "CHECKOUT_TITLES": [ "Amazon.com Checkout", @@ -115,7 +119,8 @@ "Ordine in preparazione", "Amazon.sg Checkout", "A preparar o seu pedido", - "Amazon.com.tr Alışverişi Tamamla" + "Amazon.com.tr Alışverişi Tamamla", + "Kup teraz" ], "ORDER_COMPLETE_TITLES": [ "Amazon.com Thanks You", @@ -131,7 +136,8 @@ "Thank You", "Amazon.de Vielen Dank", "Obrigado, o seu pedido foi efetuado", - "Teşekkür Ederiz" + "Teşekkür Ederiz", + "Amazon.pl Dziękujemy" ], "BUSINESS_PO_TITLES": [ "Business order information" @@ -170,7 +176,8 @@ "DELIVERY AT NO EXTRA COST FOR PRIME MEMBERS", "GRATIS LIEFERUNG FÜR PRIME-MITGLIEDER", "FREE DELIVERY:", - "SPEDIZIONE SENZA COSTI AGGIUNTIVI CON PRIME" + "SPEDIZIONE SENZA COSTI AGGIUNTIVI CON PRIME", + "DARMOWA DOSTAWA" ], "ADDRESS_SELECT": [ "Select a delivery address", From 3a71c9cda6088b858a911245cef982adc519898b Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Sat, 13 Mar 2021 17:43:33 -0500 Subject: [PATCH 142/150] Update fairgame.conf fix shopping cart title --- config/fairgame.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/fairgame.conf b/config/fairgame.conf index 08c770ba..192b5fad 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -92,7 +92,7 @@ "AmazonSmile Shopping Basket", "Amazon.nl-winkelwagen", "Amazon.com.tr Alışveriş Sepeti", - "Amazon.pl Koszyk" + "Amazon.pl: koszyk" ], "CHECKOUT_TITLES": [ "Amazon.com Checkout", From 7841669387ed246adf1fcae51299f04601a127c2 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Tue, 16 Mar 2021 13:51:26 -0400 Subject: [PATCH 143/150] Remove Best Buy and Nvidia store files no need for these anymore --- _bbuy-fe.bat | 1 - _bbuy-gbyte.bat | 1 - _bbuy-msi.bat | 1 - _nvidiadotcom.bat | 3 - cli/cli.py | 16 -- stores/bestbuy.py | 373 ---------------------------------------------- stores/nvidia.py | 271 --------------------------------- 7 files changed, 666 deletions(-) delete mode 100644 _bbuy-fe.bat delete mode 100644 _bbuy-gbyte.bat delete mode 100644 _bbuy-msi.bat delete mode 100644 _nvidiadotcom.bat delete mode 100644 stores/bestbuy.py delete mode 100644 stores/nvidia.py diff --git a/_bbuy-fe.bat b/_bbuy-fe.bat deleted file mode 100644 index f3619fc2..00000000 --- a/_bbuy-fe.bat +++ /dev/null @@ -1 +0,0 @@ -pipenv run python app.py bestbuy --sku 6429440 diff --git a/_bbuy-gbyte.bat b/_bbuy-gbyte.bat deleted file mode 100644 index 533ea9c1..00000000 --- a/_bbuy-gbyte.bat +++ /dev/null @@ -1 +0,0 @@ -pipenv run python app.py bestbuy --sku 6430621 \ No newline at end of file diff --git a/_bbuy-msi.bat b/_bbuy-msi.bat deleted file mode 100644 index 87b64f34..00000000 --- a/_bbuy-msi.bat +++ /dev/null @@ -1 +0,0 @@ -pipenv run python app.py bestbuy --sku 6432399 \ No newline at end of file diff --git a/_nvidiadotcom.bat b/_nvidiadotcom.bat deleted file mode 100644 index 942493a5..00000000 --- a/_nvidiadotcom.bat +++ /dev/null @@ -1,3 +0,0 @@ -pipenv install - -pipenv run python app.py nvidia diff --git a/cli/cli.py b/cli/cli.py index 85cae529..7e637e11 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -31,7 +31,6 @@ from common.globalconfig import AMAZON_CREDENTIAL_FILE, GlobalConfig from notifications.notifications import NotificationHandler, TIME_FORMAT from stores.amazon import Amazon -from stores.bestbuy import BestBuyHandler from utils.logger import log from utils.version import is_latest, version @@ -251,20 +250,6 @@ def amazon( time.sleep(5) -@click.command() -@click.option("--sku", type=str, required=True) -@click.option("--headless", is_flag=True) -@notify_on_crash -def bestbuy(sku, headless): - log.warning( - "As stated in the documentation, Best Buy is deprecated due to their anti-bot measures for high demand items." - ) - bb = BestBuyHandler( - sku, notification_handler=notification_handler, headless=headless - ) - bb.run_item() - - @click.option( "--disable-sound", is_flag=True, @@ -417,7 +402,6 @@ def show_traceroutes(domain): signal(SIGINT, interrupt_handler) main.add_command(amazon) -main.add_command(bestbuy) main.add_command(test_notifications) main.add_command(show) main.add_command(find_endpoints) diff --git a/stores/bestbuy.py b/stores/bestbuy.py deleted file mode 100644 index a0a7cddd..00000000 --- a/stores/bestbuy.py +++ /dev/null @@ -1,373 +0,0 @@ -# FairGame - Automated Purchasing Program -# Copyright (C) 2021 Hari Nagarajan -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# The author may be contacted through the project's GitHub, at: -# https://github.com/Hari-Nagarajan/fairgame - -import json -import webbrowser -from time import sleep - -from chromedriver_py import binary_path # this will get you the path variable -from selenium import webdriver -from selenium.webdriver.chrome.options import Options -from selenium.webdriver.support.ui import WebDriverWait - -try: - from Crypto.PublicKey import RSA - from Crypto.Cipher import PKCS1_OAEP -except: - from Cryptodome.PublicKey import RSA - from Cryptodome.Cipher import PKCS1_OAEP - -import requests -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry - -from utils.json_utils import find_values -from utils.logger import log -from utils.selenium_utils import enable_headless - -BEST_BUY_PDP_URL = "https://api.bestbuy.com/click/5592e2b895800000/{sku}/pdp" -BEST_BUY_CART_URL = "https://api.bestbuy.com/click/5592e2b895800000/{sku}/cart" - -BEST_BUY_ADD_TO_CART_API_URL = "https://www.bestbuy.com/cart/api/v1/addToCart" -BEST_BUY_CHECKOUT_URL = "https://www.bestbuy.com/checkout/c/orders/{order_id}/" - -DEFAULT_HEADERS = { - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", - "accept-encoding": "gzip, deflate, br", - "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7", - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", - "origin": "https://www.bestbuy.com", -} - -options = Options() -options.page_load_strategy = "eager" -options.add_experimental_option("excludeSwitches", ["enable-automation"]) -options.add_experimental_option("useAutomationExtension", False) -prefs = {"profile.managed_default_content_settings.images": 2} -options.add_experimental_option("prefs", prefs) -options.add_argument("user-data-dir=.profile-bb") - - -class BestBuyHandler: - def __init__(self, sku_id, notification_handler, headless=False): - self.notification_handler = notification_handler - self.sku_id = sku_id - self.session = requests.Session() - self.auto_buy = False - self.account = {"username": "", "password": ""} - - adapter = HTTPAdapter( - max_retries=Retry( - total=3, - backoff_factor=1, - status_forcelist=[429, 500, 502, 503, 504], - method_whitelist=["HEAD", "GET", "OPTIONS", "POST"], - ) - ) - self.session.mount("https://", adapter) - self.session.mount("http://", adapter) - - response = self.session.get( - BEST_BUY_PDP_URL.format(sku=self.sku_id), headers=DEFAULT_HEADERS - ) - log.info(f"PDP Request: {response.status_code}") - self.product_url = response.url - log.info(f"Product URL: {self.product_url}") - - self.session.get(self.product_url) - log.info(f"Product URL Request: {response.status_code}") - - if self.auto_buy: - log.info("Loading headless driver.") - if headless: - enable_headless() # TODO - check if this still messes up the cookies. - options.add_argument( - "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36" - ) - - self.driver = webdriver.Chrome( - executable_path=binary_path, - options=options, - ) - log.info("Loading https://www.bestbuy.com.") - self.login() - - self.driver.get(self.product_url) - cookies = self.driver.get_cookies() - - [ - self.session.cookies.set_cookie( - requests.cookies.create_cookie( - domain=cookie["domain"], - name=cookie["name"], - value=cookie["value"], - ) - ) - for cookie in cookies - ] - - # self.driver.quit() - - log.info("Calling location/v1/US/approximate") - log.info( - self.session.get( - "https://www.bestbuy.com/location/v1/US/approximate", - headers=DEFAULT_HEADERS, - ).status_code - ) - - log.info("Calling basket/v1/basketCount") - log.info( - self.session.get( - "https://www.bestbuy.com/basket/v1/basketCount", - headers={ - "x-client-id": "browse", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", - "Accept": "application/json", - }, - ).status_code - ) - - def login(self): - self.driver.get("https://www.bestbuy.com/identity/global/signin") - self.driver.find_element_by_xpath('//*[@id="fld-e"]').send_keys( - self.account["username"] - ) - self.driver.find_element_by_xpath('//*[@id="fld-p1"]').send_keys( - self.account["password"] - ) - self.driver.find_element_by_xpath( - "/html/body/div[1]/div/section/main/div[1]/div/div/div/div/form/div[3]/div/label/div/i" - ).click() - self.driver.find_element_by_xpath( - "/html/body/div[1]/div/section/main/div[1]/div/div/div/div/form/div[4]/button" - ).click() - WebDriverWait(self.driver, 10).until( - lambda x: "Official Online Store" in self.driver.title - ) - - def run_item(self): - while not self.in_stock(): - sleep(5) - log.info(f"Item {self.sku_id} is in stock!") - if self.auto_buy: - self.auto_checkout() - else: - cart_url = self.add_to_cart() - self.notification_handler.send_notification( - f"SKU: {self.sku_id} in stock: {cart_url}" - ) - sleep(5) - - def in_stock(self): - log.info("Checking stock") - url = "https://www.bestbuy.com/api/tcfb/model.json?paths=%5B%5B%22shop%22%2C%22scds%22%2C%22v2%22%2C%22page%22%2C%22tenants%22%2C%22bbypres%22%2C%22pages%22%2C%22globalnavigationv5sv%22%2C%22header%22%5D%2C%5B%22shop%22%2C%22buttonstate%22%2C%22v5%22%2C%22item%22%2C%22skus%22%2C{}%2C%22conditions%22%2C%22NONE%22%2C%22destinationZipCode%22%2C%22%2520%22%2C%22storeId%22%2C%22%2520%22%2C%22context%22%2C%22cyp%22%2C%22addAll%22%2C%22false%22%5D%5D&method=get".format( - self.sku_id - ) - response = self.session.get(url, headers=DEFAULT_HEADERS) - log.info(f"Stock check response code: {response.status_code}") - try: - response_json = response.json() - item_json = find_values( - json.dumps(response_json), "buttonStateResponseInfos" - ) - item_state = item_json[0][0]["buttonState"] - log.info(f"Item state is: {item_state}") - if item_json[0][0]["skuId"] == self.sku_id and item_state in [ - "ADD_TO_CART", - "PRE_ORDER", - ]: - return True - else: - return False - except Exception as e: - log.warning("Error parsing json. Using string search to determine state.") - log.info(response_json) - log.error(e) - if "ADD_TO_CART" in response.text: - log.info("Item is in stock!") - return True - else: - log.info("Item is out of stock") - return False - - def add_to_cart(self): - webbrowser.open_new(BEST_BUY_CART_URL.format(sku=self.sku_id)) - return BEST_BUY_CART_URL.format(sku=self.sku_id) - - def auto_checkout(self): - self.auto_add_to_cart() - self.start_checkout() - self.driver.get("https://www.bestbuy.com/checkout/c/r/fast-track") - - def auto_add_to_cart(self): - log.info("Attempting to auto add to cart...") - - body = {"items": [{"skuId": self.sku_id}]} - headers = { - "Accept": "application/json", - "authority": "www.bestbuy.com", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", - "Content-Type": "application/json; charset=UTF-8", - "Sec-Fetch-Site": "same-origin", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Dest": "empty", - "origin": "https://www.bestbuy.com", - "referer": self.product_url, - "Content-Length": str(len(json.dumps(body))), - } - # [ - # log.info({'name': c.name, 'value': c.value, 'domain': c.domain, 'path': c.path}) - # for c in self.session.cookies - # ] - log.info("Making request") - response = self.session.post( - BEST_BUY_ADD_TO_CART_API_URL, json=body, headers=headers, timeout=5 - ) - log.info(response.status_code) - if ( - response.status_code == 200 - and response.json()["cartCount"] > 0 - and self.sku_id in response.text - ): - log.info(f"Added {self.sku_id} to cart!") - log.info(response.json()) - else: - log.info(response.status_code) - log.info(response.json()) - - def start_checkout(self): - headers = { - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", - "accept-encoding": "gzip, deflate, br", - "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7", - "upgrade-insecure-requests": "1", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36", - } - while True: - log.info("Starting Checkout") - response = self.session.post( - "https://www.bestbuy.com/cart/d/checkout", headers=headers, timeout=5 - ) - if response.status_code == 200: - response_json = response.json() - log.info(response_json) - self.order_id = response_json["updateData"]["order"]["id"] - self.item_id = response_json["updateData"]["order"]["lineItems"][0][ - "id" - ] - log.info(f"Started Checkout for order id: {self.order_id}") - log.info(response_json) - if response_json["updateData"]["redirectUrl"]: - self.session.get( - response_json["updateData"]["redirectUrl"], headers=headers - ) - return - log.info("Error Starting Checkout") - sleep(5) - - def submit_shipping(self): - log.info("Starting Checkout") - headers = { - "accept": "application/json, text/javascript, */*; q=0.01", - "accept-encoding": "gzip, deflate, br", - "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7", - "content-type": "application/json", - "origin": "https://www.bestbuy.com", - "referer": "https://www.bestbuy.com/cart", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36", - "x-user-interface": "DotCom-Optimized", - "x-order-id": self.order_id, - } - while True: - log.info("Submitting Shipping") - body = {"selected": "SHIPPING"} - response = self.session.put( - "https://www.bestbuy.com/cart/item/{item_id}/fulfillment".format( - item_id=self.item_id - ), - headers=headers, - json=body, - ) - response_json = response.json() - log.info(response.status_code) - log.info(response_json) - if ( - response.status_code == 200 - and response_json["order"]["id"] == self.order_id - ): - log.info("Submitted Shipping") - return True - else: - log.info("Error Submitting Shipping") - - def submit_payment(self, tas_data): - body = { - "items": [ - { - "id": self.item_id, - "type": "DEFAULT", - "selectedFulfillment": {"shipping": {"address": {}}}, - "giftMessageSelected": False, - } - ] - } - headers = { - "accept": "application/com.bestbuy.order+json", - "accept-encoding": "gzip, deflate, br", - "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7", - "content-type": "application/json", - "origin": "https://www.bestbuy.com", - "referer": "https://www.bestbuy.com/checkout/r/fulfillment", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36", - "x-user-interface": "DotCom-Optimized", - } - r = self.session.patch( - "https://www.bestbuy.com/checkout/d/orders/{}/".format(self.order_id), - json=body, - headers=headers, - ) - [ - log.info( - {"name": c.name, "value": c.value, "domain": c.domain, "path": c.path} - ) - for c in self.session.cookies - ] - log.info(r.status_code) - log.info(r.text) - - def get_tas_data(self): - headers = { - "accept": "*/*", - "accept-encoding": "gzip, deflate, br", - "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7", - "content-type": "application/json", - "referer": "https://www.bestbuy.com/checkout/r/payment", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36", - } - while True: - try: - log.info("Getting TAS Data") - r = requests.get( - "https://www.bestbuy.com/api/csiservice/v2/key/tas", headers=headers - ) - log.info("Got TAS Data") - return json.loads(r.text) - except Exception as e: - sleep(5) diff --git a/stores/nvidia.py b/stores/nvidia.py deleted file mode 100644 index 7ac1137b..00000000 --- a/stores/nvidia.py +++ /dev/null @@ -1,271 +0,0 @@ -# FairGame - Automated Purchasing Program -# Copyright (C) 2021 Hari Nagarajan -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# The author may be contacted through the project's GitHub, at: -# https://github.com/Hari-Nagarajan/fairgame - -import concurrent -import json -import webbrowser -from concurrent.futures.thread import ThreadPoolExecutor -from datetime import datetime -from time import sleep - -import browser_cookie3 -import requests -from spinlog import Spinner - -from utils.http import TimeoutHTTPAdapter -from utils.logger import log - -NVIDIA_CART_URL = ( - "https://store.nvidia.com/store?Action=DisplayHGOP2LandingPage&SiteID=nvidia" -) -NVIDIA_TOKEN_URL = "https://store.nvidia.com/store/nvidia/SessionToken" -NVIDIA_STOCK_API = "https://api-prod.nvidia.com/direct-sales-shop/DR/products/{locale}/{currency}/{product_id}" -NVIDIA_ADD_TO_CART_API = "https://api-prod.nvidia.com/direct-sales-shop/DR/add-to-cart" - -GPU_DISPLAY_NAMES = { - "2060S": "NVIDIA GEFORCE RTX 2060 SUPER", - "3080": "NVIDIA GEFORCE RTX 3080", - "3090": "NVIDIA GEFORCE RTX 3090", -} - -CURRENCY_LOCALE_MAP = { - "en_us": "USD", - "en_gb": "GBP", - "de_de": "EUR", - "fr_fr": "EUR", - "it_it": "EUR", - "es_es": "EUR", - "nl_nl": "EUR", - "sv_se": "SEK", - "de_at": "EUR", - "fr_be": "EUR", - "da_dk": "DKK", - "cs_cz": "CZK", -} - -DEFAULT_HEADERS = { - "Accept": "application/json", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", -} -CART_SUCCESS_CODES = {201, requests.codes.ok} - - -class ProductIDChangedException(Exception): - def __init__(self): - super().__init__("Product IDS changed. We need to re run.") - - -PRODUCT_IDS_FILE = "stores/store_data/nvidia_product_ids.json" -PRODUCT_IDS = json.load(open(PRODUCT_IDS_FILE)) - - -class NvidiaBuyer: - def __init__( - self, gpu, notification_handler, locale="en_us", test=False, interval=5 - ): - self.product_ids = set([]) - self.cli_locale = locale.lower() - self.locale = self.map_locales() - self.session = requests.Session() - self.gpu = gpu - self.enabled = True - self.auto_buy_enabled = False - self.attempt = 0 - self.started_at = datetime.now() - self.test = test - self.interval = interval - - self.gpu_long_name = GPU_DISPLAY_NAMES[gpu] - - self.cj = browser_cookie3.load(".nvidia.com") - self.session.cookies = self.cj - - # Disable auto_buy_enabled if the user does not provide a bool. - if type(self.auto_buy_enabled) != bool: - self.auto_buy_enabled = False - - adapter = TimeoutHTTPAdapter() - self.session.mount("https://", adapter) - self.session.mount("http://", adapter) - self.notification_handler = notification_handler - - self.get_product_ids() - - def map_locales(self): - if self.cli_locale == "de_at": - return "de_de" - if self.cli_locale == "fr_be": - return "fr_fr" - if self.cli_locale == "da_dk": - return "en_gb" - if self.cli_locale == "cs_cz": - return "en_gb" - return self.cli_locale - - def get_product_ids(self): - if isinstance(PRODUCT_IDS[self.cli_locale][self.gpu], list): - self.product_ids = PRODUCT_IDS[self.cli_locale][self.gpu] - if isinstance(PRODUCT_IDS[self.cli_locale][self.gpu], str): - self.product_ids = [PRODUCT_IDS[self.cli_locale][self.gpu]] - - def run_items(self): - log.info( - f"We have {len(self.product_ids)} product IDs for {self.gpu_long_name}" - ) - log.info(f"Product IDs: {self.product_ids}") - try: - with ThreadPoolExecutor(max_workers=len(self.product_ids)) as executor: - product_futures = [ - executor.submit(self.buy, product_id) - for product_id in self.product_ids - ] - concurrent.futures.wait(product_futures) - for fut in product_futures: - log.debug(f"Future Result: {fut.result()}") - except ProductIDChangedException as ex: - log.warning("Product IDs changed.") - self.product_ids = set([]) - self.get_product_ids() - self.run_items() - - def buy(self, product_id): - try: - log.info(f"Stock Check {product_id} at {self.interval} second intervals.") - while not self.is_in_stock(product_id): - self.attempt = self.attempt + 1 - time_delta = str(datetime.now() - self.started_at).split(".")[0] - with Spinner.get( - f"Stock Check ({self.attempt}, have been running for {time_delta})..." - ) as s: - sleep(self.interval) - if self.enabled: - cart_success = self.add_to_cart(product_id) - if cart_success: - log.info(f"{self.gpu_long_name} added to cart.") - self.enabled = False - webbrowser.open(NVIDIA_CART_URL) - self.notification_handler.send_notification( - f" {self.gpu_long_name} with product ID: {product_id} in " - f"stock: {NVIDIA_CART_URL}" - ) - else: - self.notification_handler.send_notification( - f" ERROR: Attempted to add {self.gpu_long_name} to cart but couldn't, check manually!" - ) - self.buy(product_id) - except requests.exceptions.RequestException as e: - log.warning("Connection error while calling Nvidia API. API may be down.") - log.info( - f"Got an unexpected reply from the server, API may be down, nothing we can do but try again" - ) - self.buy(product_id) - - def is_in_stock(self, product_id): - try: - response = self.session.get( - NVIDIA_STOCK_API.format( - product_id=product_id, - locale=self.locale, - currency=CURRENCY_LOCALE_MAP.get(self.locale, "USD"), - cookies=self.cj, - ), - headers=DEFAULT_HEADERS, - ) - log.debug(f"Stock check response code: {response.status_code}") - if response.status_code != 200: - log.debug(response.text) - if "PRODUCT_INVENTORY_IN_STOCK" in response.text: - return True - else: - return False - except requests.exceptions.RequestException as e: - log.info( - f"Got an unexpected reply from the server, API may be down, nothing we can do but try again" - ) - return False - - def add_to_cart(self, product_id): - try: - success, token = self.get_session_token() - if not success: - return False - log.info(f"Session token: {token}") - - data = {"products": [{"productId": product_id, "quantity": 1}]} - headers = DEFAULT_HEADERS.copy() - headers["locale"] = self.locale - headers["nvidia_shop_id"] = token - headers["Content-Type"] = "application/json" - response = self.session.post( - url=NVIDIA_ADD_TO_CART_API, - headers=headers, - data=json.dumps(data), - cookies=self.cj, - ) - if response.status_code == 200: - response_json = response.json() - print(response_json) - if "successfully" in response_json["message"]: - return True - else: - log.error(response.text) - log.error( - f"Add to cart failed with {response.status_code}. This is likely an error with nvidia's API." - ) - return False - except requests.exceptions.RequestException as e: - log.info(e) - log.info( - f"Got an unexpected reply from the server, API may be down, nothing we can do but try again" - ) - return False - - def get_session_token(self): - """ - Ok now this works, but I dont know when the cookies expire so might be unstable. - :return: - """ - - params = {"format": "json", "locale": self.locale} - headers = DEFAULT_HEADERS.copy() - headers["locale"] = self.locale - headers["cookie"] = "; ".join( - [f"{cookie.name}={cookie.value}" for cookie in self.session.cookies] - ) - - try: - response = self.session.get( - NVIDIA_TOKEN_URL, - headers=headers, - params=params, - cookies=self.cj, - ) - if response.status_code == 200: - response_json = response.json() - if "session_token" not in response_json: - log.error("Error getting session token.") - return False, "" - return True, response_json["session_token"] - else: - log.debug(f"Get Session Token: {response.status_code}") - except requests.exceptions.RequestException as e: - log.info( - f"Got an unexpected reply from the server, API may be down, nothing we can do but try again" - ) - return False From 4a497916110da3cc7f33c3f789854a90cbcc7806 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Thu, 18 Mar 2021 14:18:20 -0400 Subject: [PATCH 144/150] Captcha update add some more titles for captcha check, update to use latest amazoncaptcha module. --- Pipfile | 2 +- config/fairgame.conf | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 5eb8b76c..11418b9b 100644 --- a/Pipfile +++ b/Pipfile @@ -23,7 +23,7 @@ prompt_toolkit = "*" aiohttp = "*" pyobjc = { version = "*", sys_platform = "== 'darwin'" } async-timeout = "*" -amazoncaptcha = "==0.4.4" +amazoncaptcha = "*" browser-cookie3 = "*" coloredlogs = "*" apprise = "*" diff --git a/config/fairgame.conf b/config/fairgame.conf index 192b5fad..946fb238 100644 --- a/config/fairgame.conf +++ b/config/fairgame.conf @@ -60,7 +60,9 @@ ], "CAPTCHA_PAGE_TITLES": [ "Robot Check", - "Server Busy" + "Server Busy", + "Amazon.com", + "Amazon.ca" ], "HOME_PAGE_TITLES": [ "Amazon.com: Online Shopping for Electronics, Apparel, Computers, Books, DVDs & more", From 02bbade729aabaf50ebe6203455e8c75560e6b9b Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Thu, 18 Mar 2021 14:45:03 -0400 Subject: [PATCH 145/150] Update amazon.py captcha code update some captcha code, add user intervention time, add cli arg for waiting --- cli/cli.py | 8 +++++++ stores/amazon.py | 60 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/cli/cli.py b/cli/cli.py index 7e637e11..75947578 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -189,6 +189,12 @@ def main(): default=False, help="Directly hit the offers page. Preferred, but deprecated by Amazon.", ) +@click.option( + "--captcha-wait", + is_Flag=True, + default=False, + help="Wait if captcha could not be solved. Only occurs if enters captcha handler during checkout." +) @notify_on_crash def amazon( no_image, @@ -209,6 +215,7 @@ def amazon( clean_profile, clean_credentials, alt_offers, + captcha_wait, ): notification_handler.sound_enabled = not disable_sound if not notification_handler.sound_enabled: @@ -241,6 +248,7 @@ def amazon( log_stock_check=log_stock_check, shipping_bypass=shipping_bypass, alt_offers=alt_offers, + wait_on_captcha_fail=captcha_wait ) try: amzn_obj.run(delay=delay, test=test) diff --git a/stores/amazon.py b/stores/amazon.py index 53bcce34..861aaa18 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -109,6 +109,7 @@ def __init__( log_stock_check=False, shipping_bypass=False, alt_offers=False, + wait_on_captcha_fail=False, ): self.notification_handler = notification_handler self.asin_list = [] @@ -138,6 +139,7 @@ def __init__( self.shipping_bypass = shipping_bypass self.unknown_title_notification_sent = False self.alt_offers = alt_offers + self.wait_on_captcha_fail = wait_on_captcha_fail presence.enabled = not disable_presence @@ -1437,26 +1439,52 @@ def handle_captcha(self, check_presence=True): log.info( f"Failed to solve {captcha.image_link}, lets reload and get a new captcha." ) - self.driver.refresh() - time.sleep(3) + if self.wait_on_captcha_fail: + log.info("Will wait up to 60 seconds for user to solve captcha") + self.send("User Intervention Required - captcha check", "captcha", self.take_screenshots) + with self.wait_for_page_content_change(): + timeout = self.get_timeout(timeout=60) + while time.time() < timeout and self.driver.title == current_page: + time.sleep(0.5) + # check above is not true, then we must have passed captcha, return back to nav handler + # Otherwise refresh page to try again - either way, returning to nav page handler + if time.time() >timeout and self.driver.title == current_page: + log.info("User intervention did not occur in time - will attempt to refresh page and try again") + self.driver.refresh() + return False + else: + return True + # Solved (!?) else: - self.send_notification( - "Solving catpcha", "captcha", self.take_screenshots - ) - self.driver.find_element_by_xpath( - '//*[@id="captchacharacters"]' - ).send_keys(solution + Keys.RETURN) - self.wait_for_page_change(page_title=current_page) + # take screenshot if user asked for detailed + if self.detailed: + self.send_notification( + "Solving catpcha", "captcha", self.take_screenshots + ) + try: + captcha_field = self.driver.find_element_by_xpath( + '//*[@id="captchacharacters"]' + ) + except sel_exceptions.NoSuchElementException: + log.debug("Could not locate captcha") + captcha_field = None + if captcha_field: + with self.wait_for_page_content_change(): + captcha_field.send_keys(solution + Keys.RETURN) + return True + else: + return False except Exception as e: log.debug(e) - log.info("Error trying to solve captcha. Refresh and retry.") - self.driver.refresh() - time.sleep(3) + log.debug("Error trying to solve captcha. Refresh and retry.") + with self.wait_for_page_content_change(): + self.driver.refresh() + return False except sel_exceptions.NoSuchElementException: - log.error("captcha page does not contain captcha element") - log.error("refreshing") - self.driver.refresh() - time.sleep(3) + log.debug("captcha page does not contain captcha element") + with self.wait_for_page_content_change(): + self.driver.refresh() + return False @debug def handle_business_po(self): From 30a33cddc2875d12d7a3a5e7029511162d1b8b72 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Thu, 18 Mar 2021 15:10:03 -0400 Subject: [PATCH 146/150] Update cli.py typo --- cli/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/cli.py b/cli/cli.py index 75947578..47e73757 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -191,9 +191,9 @@ def main(): ) @click.option( "--captcha-wait", - is_Flag=True, + is_flag=True, default=False, - help="Wait if captcha could not be solved. Only occurs if enters captcha handler during checkout." + help="Wait if captcha could not be solved. Only occurs if enters captcha handler during checkout.", ) @notify_on_crash def amazon( From 6ba3a75cfec228f23f11191a33f0166e05e6d839 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Thu, 18 Mar 2021 19:18:38 -0400 Subject: [PATCH 147/150] Update amazon.py add notification sound for purchase when order complete --- stores/amazon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stores/amazon.py b/stores/amazon.py index 861aaa18..3c67b8ae 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1400,6 +1400,7 @@ def handle_checkout(self, test): def handle_order_complete(self): log.info("Order Placed.") self.send_notification("Order placed.", "order-placed", self.take_screenshots) + self.notification_handler.play_purchase_sound() self.great_success = True if self.single_shot: self.asin_list = [] From f7c49762aa5f5081fe3af32811e265a076b8b88d Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 19 Mar 2021 16:35:46 -0400 Subject: [PATCH 148/150] Update amazon.py fix param to force flyout open --- stores/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/amazon.py b/stores/amazon.py index 3c67b8ae..28328258 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -463,7 +463,7 @@ def check_stock(self, asin, reserve_min, reserve_max, retry=0): ) else: # Force the flyout by default - f = furl(self.ACTIVE_OFFER_URL + asin + "/#aod") + f = furl(self.ACTIVE_OFFER_URL + asin + "?aod=1") fail_counter = 0 presence.searching_update() From a06ab519cbceaa53fa4836b5f7c6000885759cc4 Mon Sep 17 00:00:00 2001 From: DakkJaniels <6080734+DakkJaniels@users.noreply.github.com> Date: Fri, 19 Mar 2021 16:37:26 -0400 Subject: [PATCH 149/150] Update version.py update version number for pull to master --- utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version.py b/utils/version.py index 49546e01..8a320d0f 100644 --- a/utils/version.py +++ b/utils/version.py @@ -27,7 +27,7 @@ # See https://www.python.org/dev/peps/pep-0440/ for specification # See https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes for examples -__VERSION = "0.6.1.dev5" +__VERSION = "0.6.1" version = Version(__VERSION) From 83bd7a66aa2414b270ecf6430720ba9c8782098c Mon Sep 17 00:00:00 2001 From: unapproachable Date: Fri, 19 Mar 2021 16:46:39 -0400 Subject: [PATCH 150/150] blackd --- cli/cli.py | 2 +- stores/amazon.py | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/cli/cli.py b/cli/cli.py index 47e73757..39b764e8 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -248,7 +248,7 @@ def amazon( log_stock_check=log_stock_check, shipping_bypass=shipping_bypass, alt_offers=alt_offers, - wait_on_captcha_fail=captcha_wait + wait_on_captcha_fail=captcha_wait, ) try: amzn_obj.run(delay=delay, test=test) diff --git a/stores/amazon.py b/stores/amazon.py index 28328258..c6383783 100644 --- a/stores/amazon.py +++ b/stores/amazon.py @@ -1441,16 +1441,30 @@ def handle_captcha(self, check_presence=True): f"Failed to solve {captcha.image_link}, lets reload and get a new captcha." ) if self.wait_on_captcha_fail: - log.info("Will wait up to 60 seconds for user to solve captcha") - self.send("User Intervention Required - captcha check", "captcha", self.take_screenshots) + log.info( + "Will wait up to 60 seconds for user to solve captcha" + ) + self.send( + "User Intervention Required - captcha check", + "captcha", + self.take_screenshots, + ) with self.wait_for_page_content_change(): timeout = self.get_timeout(timeout=60) - while time.time() < timeout and self.driver.title == current_page: + while ( + time.time() < timeout + and self.driver.title == current_page + ): time.sleep(0.5) # check above is not true, then we must have passed captcha, return back to nav handler # Otherwise refresh page to try again - either way, returning to nav page handler - if time.time() >timeout and self.driver.title == current_page: - log.info("User intervention did not occur in time - will attempt to refresh page and try again") + if ( + time.time() > timeout + and self.driver.title == current_page + ): + log.info( + "User intervention did not occur in time - will attempt to refresh page and try again" + ) self.driver.refresh() return False else: