From ecc63084c05fa64bef0a8a39eaaa52e095201389 Mon Sep 17 00:00:00 2001 From: Steven Mirabito Date: Sat, 16 Nov 2019 23:34:39 -0800 Subject: [PATCH] Update GameStop provider --- README.md | 2 +- balance_check/config.py | 4 +- balance_check/providers/gamestop.py | 123 ++++++++++++++-------------- 3 files changed, 66 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index c069b48..f3f01ec 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Automation tool for checking the balance of gift cards issued by various provide ## Supported Providers - [Blackhawk / Gift Card Mall / Simon / 5Back](https://mygift.giftcardmall.com) - `blackhawk` CAPTCHA - [Spafinder](https://www.spafinder.com/pages/card-balance-inquiry/vpln) - `spafinder` CAPTCHA -- [GameStop](https://www.gamestop.com/profiles/valuelookup.aspx) - `gamestop` CAPTCHA +- [GameStop](https://www.gamestop.com/giftcards/) - `gamestop` CAPTCHA - [Best Buy](https://www.bestbuy.com/gift-card-balance) - `bestbuy` - [Home Depot](https://www.homedepot.com/mycheckout/giftcard) - `homedepot` CAPTCHA diff --git a/balance_check/config.py b/balance_check/config.py index 34786f3..fd596f8 100644 --- a/balance_check/config.py +++ b/balance_check/config.py @@ -6,8 +6,8 @@ USER_AGENT = env.get( "USER_AGENT", ( - "Mozilla/5.0 (Macintosh;" - "Intel Mac OS X 10.14;" + "Mozilla/5.0 (Macintosh; " + "Intel Mac OS X 10.14; " "rv:64.0) Gecko/20100101 Firefox/64.0" ), ) diff --git a/balance_check/providers/gamestop.py b/balance_check/providers/gamestop.py index 1b30b08..d4bd656 100644 --- a/balance_check/providers/gamestop.py +++ b/balance_check/providers/gamestop.py @@ -1,52 +1,55 @@ -import requests +import json +from urllib import request +from urllib.parse import urlencode from bs4 import BeautifulSoup +from http.cookiejar import CookieJar from balance_check import logger, config +from balance_check.utils import deep_get from balance_check.utils.captcha import CaptchaSolver from balance_check.providers import BalanceCheckProvider from balance_check.validators.gift_card import Merchant, GiftCardSchema +HEADERS = { + "user-agent": config.USER_AGENT, +} + class GameStop(BalanceCheckProvider): def __init__(self): super().__init__() - self.website_url = "https://www.gamestop.com/profiles/valuelookup.aspx" + self.website_url = "https://www.gamestop.com/giftcards/" + self.api_endpoint = "https://www.gamestop.com/on/demandware.store/Sites-gamestop-us-Site/default/GiftCard-BalanceCheck" self.schema = GiftCardSchema(Merchant.GameStop) self.max_workers = 1 # For some reason does not work properly multithreaded - def scrape(self, fields): - session = requests.Session() - session.headers.update({"User-Agent": config.USER_AGENT}) + def scrape(self, **kwargs): + cookies = CookieJar() + opener = request.build_opener(request.HTTPCookieProcessor(cookies)) logger.info("Fetching balance check page") - resp = session.get(self.website_url) - if resp.status_code != 200: + # Use urllib directly as requests gets blocked + req = request.Request(self.website_url, headers=HEADERS) + resp = opener.open(req) + + if resp.status != 200: raise RuntimeError( - f"Failed to GET website (status code {resp.status_code})" + f"Failed to get GameStop website (status code {resp.status})" ) - page_html = BeautifulSoup(resp.content, features="html.parser") - form = page_html.find("form") - if not form: - raise RuntimeError("Unable to find balance check form") - - endpoint = f"{self.website_url}{form['action']}" + page_html = BeautifulSoup(resp.read(), features="html.parser") - # These fields are present on GS balance check page, does not work without including them - fields["__EVENTTARGET"] = "" - fields["__EVENTARGUMENT"] = "" - fields["__LASTFOCUS"] = "" - fields["__VIEWSTATE"] = page_html.find("input", id="__VIEWSTATE")["value"] - fields["__VIEWSTATEGENERATOR"] = page_html.find( - "input", id="__VIEWSTATEGENERATOR" - )["value"] + recaptcha_el = page_html.find("div", {"data-sitekey": True}) + if not recaptcha_el: + raise RuntimeError("Unable to find reCAPTCHA on page") - recaptcha_field = page_html.find("div", class_="g-recaptcha") - if not recaptcha_field: - raise RuntimeError("Unable to find reCAPTCHA") + csrf_el = page_html.find("input", {"name": "csrf_token"}) + if not csrf_el: + raise RuntimeError("Unable to find CSRF on page") - site_key = recaptcha_field["data-sitekey"] + site_key = recaptcha_el["data-sitekey"] + csrf_token = csrf_el["value"] logger.info("Solving reCAPTCHA (~30s)") @@ -57,51 +60,51 @@ def scrape(self, fields): f"Unable to solve reCAPTCHA ({captcha_resp['errorDescription']})" ) - fields["g-recaptcha-response"] = captcha_resp["solution"]["gRecaptchaResponse"] + payload = { + "dwfrm_giftCard_balance_accountNumber": kwargs["card_number"], + "dwfrm_giftCard_balance_pinNumber": kwargs["pin"], + "g-recaptcha-response": captcha_resp["solution"]["gRecaptchaResponse"], + "csrf_token": csrf_token, + } - logger.info("Fetching card balance") + logger.info("Fetching balance from API") - form_resp = session.post(endpoint, data=fields) - if form_resp.status_code != 200: - raise RuntimeError( - f"Failed to retrieve card balance (status code {form_resp.status_code})" + try: + req = request.Request(self.api_endpoint, headers=HEADERS) + data = urlencode(payload).encode("utf-8") + req.add_header( + "Content-Type", "application/x-www-form-urlencoded; charset=UTF-8" ) + req.add_header("Content-Length", len(data)) + resp = opener.open(req, data) - balance_html = BeautifulSoup(form_resp.content, features="html.parser") + if resp.status != 200: + raise RuntimeError(f"Invalid API response (status code {resp.status})") - try: - avail_balance = balance_html.find("span", class_="balancePrice").text - except: - # GS prunes old depleted cards from its system and throws an 'invalid' error - # Set these as -1 to notate they gave invalid error but are likely actually zero balance - if balance_html.find(text="The Gift Card number entered is invalid."): - avail_balance = "-1" - elif balance_html.find(text="The PIN number entered is invalid."): - raise RuntimeError("Invalid PIN") - elif balance_html.find(text="The code you entered is invalid."): - raise RuntimeError("Invalid CAPTCHA answer supplied") - else: + result = json.loads( + resp.read().decode(resp.info().get_param("charset") or "utf-8") + ) + errors = deep_get(result, "error") + if errors: + err = errors[0] + raise RuntimeError(f"Failed to retrieve balance from API: {err}") + + balance = deep_get(result, "balance") + if balance is None: raise RuntimeError( - "Could not find balance on retrieved page for unknown reason" + f"Invalid API response: unable to find required key in JSON response" ) - logger.info(f"Success! Card balance: {avail_balance}") + logger.info(f"Success! Card balance: ${balance}") - return {"balance": avail_balance} + return { + "balance": f"${balance}", + } + except json.JSONDecodeError: + raise RuntimeError("Failed to parse API response as JSON") def check_balance(self, **kwargs): if self.validate(kwargs): logger.info(f"Checking balance for card: {kwargs['card_number']}") - form_inputs = { - "ctl00$ctl00$BaseContentPlaceHolder$mainContentPlaceHolder$CardNumberTextBox": kwargs[ - "card_number" - ], - "ctl00$ctl00$BaseContentPlaceHolder$mainContentPlaceHolder$PinTextBox": kwargs[ - "pin" - ], - } - - return self.scrape(form_inputs) - # else: - # Invalid + return self.scrape(card_number=kwargs["card_number"], pin=kwargs["pin"])