Skip to content
This repository has been archived by the owner on Jul 15, 2021. It is now read-only.

Commit

Permalink
Use cached product IDs + refactor API checkout flow
Browse files Browse the repository at this point in the history
  • Loading branch information
philippnormann committed Oct 26, 2020
1 parent 2aa2141 commit 5f815a0
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 106 deletions.
165 changes: 84 additions & 81 deletions sniper/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,68 +38,9 @@ def read_json(filename):
return json.load(json_file)


async def checkout_api(driver, client, notifications):
logging.info(
f"Checking {client.promo_locale} availability for {client.target_gpu['name']} using API...")
product_loaded = checkout.get_product_page(
driver, client.promo_locale, client.target_gpu, anticache=True)
if product_loaded:
try:
item = driver.find_element(
By.CSS_SELECTOR, const.PRODUCT_ITEM_SELECTOR)
dr_id = item.get_attribute('data-digital-river-id')
except NoSuchElementException:
logging.info('Failed to locate Digital River ID on product page')
return False
try:
inventory = await client.get_inventory_status(dr_id)
except Exception as ex:
logging.error(
f'Failed to get inventory status for {dr_id}, {type(ex).__name__}: ' + ','.join(ex.args))
return False
logging.info(f'Inventory status for {dr_id}: {inventory}')
if inventory != 'PRODUCT_INVENTORY_OUT_OF_STOCK':
logging.info(f"Found available GPU: {client.target_gpu['name']}")
if notifications['availability']['enabled']:
driver.save_screenshot(const.SCREENSHOT_FILE)
notifications.queue.put('availability')
try:
logging.info('Fetching API token...')
store_token = await client.fetch_token()
logging.info('API Token: ' + store_token)
logging.info('Overiding store cookies for driver...')
store_cookies = client.get_cookies(const.STORE_URL)
driver.get(const.STORE_URL)
for key, value in store_cookies.items():
driver.add_cookie({'name': key, 'value': value})
except Exception as ex:
logging.error(
f'Failed to fetch API token, {type(ex).__name__}: ' + ','.join(ex.args))
return False
try:
logging.info('Calling add to cart API...')
add_to_cart_response = await client.add_to_cart(store_token, dr_id)
response = add_to_cart_response['message']
logging.info(f'Add to basket response: {response}')
except Exception as ex:
logging.error(
f'Failed to add item to basket, {type(ex).__name__}: ' + ','.join(ex.args))
return False
try:
logging.info('Going to checkout page...')
driver.get(const.CHECKOUT_URL)
if notifications['add-to-basket']['enabled']:
driver.save_screenshot(const.SCREENSHOT_FILE)
notifications.queue.put('add-to-basket')
return True
except (TimeoutException, WebDriverException):
logging.error(
'Lost basket and failed to checkout, trying again...')
return False
else:
return False
else:
return False
def update_sku_file(skus):
with open(data_path / 'skus.json', 'w+') as f:
f.write(json.dumps(skus, indent=4))


def read_config():
Expand Down Expand Up @@ -143,9 +84,9 @@ async def main():
format=log_format, handlers=[fh, sh])

gpu_data = read_json(data_path / 'gpus.json')
target_gpu, _ = pick(list(gpu_data.keys()),
'Which GPU are you targeting?',
indicator='=>')
target_gpu_name, _ = pick(list(gpu_data.keys()),
'Which GPU are you targeting?',
indicator='=>')

payment_method, _ = pick(['credit-card', 'paypal'],
'Which payment method do you want to use?',
Expand All @@ -162,7 +103,7 @@ async def main():
'Please choose a timout / refresh interval', indicator='=>', default_index=1)
timeout = int(timeout.replace('seconds', '').strip())

target_gpu = gpu_data[target_gpu]
target_gpu = gpu_data[target_gpu_name]

notifications = notification_config['notifications']
notification_queue = queue.Queue()
Expand All @@ -178,6 +119,9 @@ async def main():
api_client = api.Client(user_agent, promo_locale,
dr_locale, api_currency, target_gpu)

product_ids = read_json(data_path / 'skus.json')
target_id = product_ids[promo_locale][target_gpu_name]

logging.info('|---------------------------|')
logging.info('| Starting Nvidia Sniper 🎯 |')
logging.info(f'| Customer locale: {locale} |')
Expand All @@ -188,27 +132,86 @@ async def main():

if notifications['started']['enabled']:
checkout.get_product_page(driver, promo_locale, target_gpu)
WebDriverWait(driver, timeout).until(EC.presence_of_element_located(
(By.CLASS_NAME, const.BANNER_CLASS)))
WebDriverWait(driver, timeout).until(EC.visibility_of_element_located(
(By.CSS_SELECTOR, f'.{const.BANNER_CLASS} .lazyloaded')))
sleep(1)
driver.save_screenshot(const.SCREENSHOT_FILE)
notification_queue.put('started')

# Check if user is using recaptcha extension
if os.path.isfile('./recaptcha_solver-5.7-fx.xpi'):
logging.info('ReCaptcha solver detected, enabled')
# Must be the full path to an XPI file!
extension_path = os.path.abspath("recaptcha_solver-5.7-fx.xpi")
driver.install_addon(extension_path, temporary=True)
else:
logging.info('ReCaptcha solver not found')

while True:
checkout_reached = await checkout_api(driver, api_client, notifications)
in_stock = False
try:
logging.info(
f"Checking {promo_locale} availability for {target_gpu['name']} using API...")
status = await api_client.check_availability(target_id)
logging.info(f'Inventory status for {target_id}: {status}')
in_stock = status != 'PRODUCT_INVENTORY_OUT_OF_STOCK'
except LookupError:
logging.error(
f'Failed to get inventory status for {target_id}, updating product ID...')
target_id = None
while target_id is None:
target_id = await api_client.get_product_id()
if target_id is None:
logging.error(
f"Failed to locate product ID for {target_gpu['name']}, trying again...")
sleep(timeout)
else:
logging.info(
f"Found product ID for {target_gpu['name']}: {target_id}")
product_ids[promo_locale][target_gpu_name] = target_id
logging.info('Updating product ID file...')
update_sku_file(product_ids)
except SystemError as ex:
logging.error(
f'Internal API error, {type(ex).__name__}: ' + ','.join(ex.args))

if in_stock:
logging.info(
f"Found available GPU: {target_gpu['name']}")
if notifications['availability']['enabled']:
notification_queue.put('availability')
store_token = None
while store_token is None:
try:
logging.info('Fetching API token...')
store_token = await api_client.get_token()
logging.info('API Token: ' + store_token)
logging.info('Overiding store cookies for driver...')
store_cookies = api_client.get_cookies(const.STORE_URL)
driver.get(const.STORE_URL)
for key, value in store_cookies.items():
driver.add_cookie({'name': key, 'value': value})
except SystemError:
logging.error('Failed to fetch API token, trying again...')

addded_to_cart = False
while not addded_to_cart:
try:
logging.info('Calling add to cart API...')
add_to_cart_response = await api_client.add_to_cart(store_token, target_id)
addded_to_cart = True
response = add_to_cart_response['message']
logging.info(f'Add to cart response: {response}')
except Exception as ex:
logging.error(
f'Failed to add item to cart, {type(ex).__name__}: ' + ','.join(ex.args))

checkout_reached = False
while not checkout_reached:
try:
logging.info('Going to checkout page...')
driver.get(const.CHECKOUT_URL)
checkout_reached = True
if notifications['add-to-basket']['enabled']:
driver.save_screenshot(const.SCREENSHOT_FILE)
notification_queue.put('add-to-basket')
except (TimeoutException, WebDriverException):
logging.error(
'Failed to load checkout page, trying again...')

if checkout_reached:
if payment_method == 'credit-card':
checkout.checkout_guest(
driver, timeout, customer, auto_submit)
checkout.checkout_guest(driver, timeout, customer, auto_submit)
else:
checkout.checkout_paypal(driver, timeout),

Expand Down
30 changes: 21 additions & 9 deletions sniper/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import aiohttp

from bs4 import BeautifulSoup

import sniper.constants as const


Expand All @@ -18,7 +20,25 @@ def get_cookies(self, host):
cookies = self.session.cookie_jar.filter_cookies(host)
return {key: morsel.value for key, morsel in cookies.items()}

async def fetch_token(self):
async def check_availability(self, dr_id):
async with self.session.get(f'{const.INVENTORY_URL}/{self.dr_locale}/{self.api_currency}/{dr_id}') as response:
if response.status == 200:
json_resp = await response.json()
return json_resp['products']['product'][0]['inventoryStatus']['status']
elif 400 <= response.status < 500:
raise(LookupError(await response.text()))
elif 500 <= response.status < 600:
raise(SystemError(await response.text()))

async def get_product_id(self):
full_url = f"https://www.nvidia.com/{self.promo_locale}{self.target_gpu['url']}"
async with self.session.get(full_url) as resp:
soup = BeautifulSoup(await resp.text(), features="html.parser")
product = soup.select_one(const.PRODUCT_ITEM_SELECTOR,
attrs={const.PRODUCT_ID_ATTR: True})
return product[const.PRODUCT_ID_ATTR] if product is not None else None

async def get_token(self):
async with self.session.get(f'{const.TOKEN_URL}?locale={self.dr_locale}') as response:
if response.status == 200:
json_resp = await response.json()
Expand All @@ -34,11 +54,3 @@ async def add_to_cart(self, store_token, dr_id):
return await response.json()
else:
raise(SystemError(await response.text()))

async def get_inventory_status(self, dr_id):
async with self.session.get(f'{const.INVENTORY_URL}/{self.dr_locale}/{self.api_currency}/{dr_id}') as response:
if response.status == 200:
json_resp = await response.json()
return json_resp['products']['product'][0]['inventoryStatus']['status']
else:
raise(SystemError(await response.text()))
24 changes: 9 additions & 15 deletions sniper/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from selenium.webdriver.common.by import By
except Exception:
logging.error(
'Could not import all required modules. '\
'Please run the following command again:\n\n'\
'Could not import all required modules. '
'Please run the following command again:\n\n'
'\tpipenv install\n')
exit()

Expand All @@ -25,16 +25,9 @@ def scroll_to(driver, element):
'arguments[0].scrollIntoView({block: "center"})', element)


def get_product_page(driver, promo_locale, gpu, anticache=False):
anticache_key = ''.join(random.choice(string.ascii_lowercase)
for i in range(5))
anticache_value = random.randint(0, 9999)
anticache_query = f'?{anticache_key}={anticache_value}'
full_url = f"https://www.nvidia.com/{promo_locale}{gpu['url']}"
if anticache:
full_url += anticache_query
def get_product_page(driver, promo_locale, gpu):
try:
driver.get(full_url)
driver.get(f"https://www.nvidia.com/{promo_locale}{gpu['url']}")
return True
except (TimeoutException, WebDriverException):
return False
Expand Down Expand Up @@ -87,13 +80,13 @@ def fill_out_form(driver, timeout, customer):
if customer['shipping']['backup-speed']:
logging.info('Continuing with default speed')
else:
logging.info('User opted to stop if shipping speed not found.')
logging.info(
'User opted to stop if shipping speed not found.')
exit()
else:
logging.warning(
'data/customer.json missing "backup-speed" option under "shipping", '\
'data/customer.json missing "backup-speed" option under "shipping", '
'continuing with default speed')


shipping_expanded = False
while not shipping_expanded:
Expand Down Expand Up @@ -145,7 +138,8 @@ def fill_out_form(driver, timeout, customer):
customer['credit']['card'])

month_select = Select(driver.find_element_by_id('expirationDateMonth'))
month_select.select_by_value(customer['credit']['expiration']['month'].lstrip('0'))
month_select.select_by_value(
customer['credit']['expiration']['month'].lstrip('0'))

year_select = Select(driver.find_element_by_id('expirationDateYear'))
year_select.select_by_value(customer['credit']['expiration']['year'])
Expand Down
1 change: 1 addition & 0 deletions sniper/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
BANNER_CLASS = 'singleSlideBanner'
ADD_TO_BASKET_SELECTOR = f'.{BANNER_CLASS} .js-add-button'
PRODUCT_ITEM_SELECTOR = f'.{BANNER_CLASS} .js-product-item'
PRODUCT_ID_ATTR = 'data-digital-river-id'
CHECKOUT_BUTTON_SELECTOR = '.cart .js-checkout'
CART_ICON_CLASS = 'nav-cart-link'
CHECKOUT_AS_GUEST_ID = 'btnCheckoutAsGuest'
Expand Down
9 changes: 8 additions & 1 deletion sniper/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,11 @@ def prepare_sniper_profile(default_profile_path):
def create():
default_profile_path = get_default_profile()
profile = prepare_sniper_profile(default_profile_path)
return webdriver.Firefox(firefox_profile=profile, executable_path=GeckoDriverManager().install())
driver = webdriver.Firefox(firefox_profile=profile, executable_path=GeckoDriverManager().install())
if os.path.isfile('./recaptcha_solver-5.7-fx.xpi'):
logging.info('ReCaptcha solver detected, enabled')
extension_path = os.path.abspath("recaptcha_solver-5.7-fx.xpi")
driver.install_addon(extension_path, temporary=True)
else:
logging.info('ReCaptcha solver not found')
return driver

0 comments on commit 5f815a0

Please sign in to comment.