-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
277 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.idea | ||
venv | ||
build | ||
dist | ||
*.pyc | ||
*.spec | ||
config.ini |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import asyncio | ||
import json | ||
import KrogerCLI | ||
from pyppeteer import launch | ||
|
||
|
||
class KrogerAPI: | ||
browser_options = { | ||
'headless': True, | ||
'args': ['--blink-settings=imagesEnabled=false'] # Disable images for hopefully faster load-time | ||
} | ||
headers = { | ||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' | ||
'Chrome/81.0.4044.129 Safari/537.36', | ||
'Accept-Language': 'en-US,en;q=0.9' | ||
} | ||
|
||
def __init__(self, cli: KrogerCLI): | ||
self.cli = cli | ||
|
||
def get_account_info(self): | ||
return asyncio.run(self._get_account_info()) | ||
|
||
def clip_coupons(self): | ||
return asyncio.run(self._clip_coupons()) | ||
|
||
async def _get_account_info(self): | ||
signed_in = await self.sign_in_routine() | ||
if not signed_in: | ||
await self.destroy() | ||
return None | ||
|
||
self.cli.console.print('Loading profile info..') | ||
await self.page.goto('https://www.' + self.cli.config['main']['domain'] + '/accountmanagement/api/profile') | ||
try: | ||
plain_text = await self.page.plainText() | ||
profile = json.loads(plain_text) | ||
user_id = profile['userId'] | ||
except Exception: | ||
profile = None | ||
await self.destroy() | ||
|
||
return profile | ||
|
||
async def _clip_coupons(self): | ||
signed_in = await self.sign_in_routine(redirect_url='/cl/coupons/', contains=['Coupons Clipped']) | ||
if not signed_in: | ||
await self.destroy() | ||
return None | ||
|
||
js = """ | ||
window.scrollTo(0, document.body.scrollHeight); | ||
for (let i = 0; i < 150; i++) { | ||
let el = document.getElementsByClassName('kds-Button--favorable')[i]; | ||
if (el !== undefined) { | ||
el.scrollIntoView(); | ||
el.click(); | ||
} | ||
} | ||
""" | ||
|
||
self.cli.console.print('[italic]Applying the coupons, please wait..[/italic]') | ||
await self.page.keyboard.press('Escape') | ||
for i in range(6): | ||
await self.page.evaluate(js) | ||
await self.page.keyboard.press('End') | ||
await self.page.waitFor(1000) | ||
await self.page.waitFor(3000) | ||
await self.destroy() | ||
self.cli.console.print('[bold]Coupons successfully clipped to your account! :thumbs_up:[/bold]') | ||
|
||
async def init(self): | ||
self.browser = await launch(self.browser_options) | ||
self.page = await self.browser.newPage() | ||
await self.page.setExtraHTTPHeaders(self.headers) | ||
await self.page.setViewport({'width': 700, 'height': 0}) | ||
|
||
async def destroy(self): | ||
await self.browser.close() | ||
|
||
async def sign_in_routine(self, redirect_url='/account/update', contains=None): | ||
await self.init() | ||
self.cli.console.print('[italic]Signing in.. (please wait, it might take awhile)[/italic]') | ||
signed_in = await self.sign_in(redirect_url, contains) | ||
|
||
if not signed_in and self.browser_options['headless']: | ||
self.cli.console.print('[red]Sign in failed. Trying one more time..[/red]') | ||
self.browser_options['headless'] = False | ||
await self.destroy() | ||
await self.init() | ||
signed_in = await self.sign_in(redirect_url, contains) | ||
|
||
if not signed_in: | ||
self.cli.console.print('[bold red]Sign in failed. Please make sure the username/password is correct.' | ||
'[/bold red]') | ||
|
||
return signed_in | ||
|
||
async def sign_in(self, redirect_url, contains): | ||
timeout = 20000 | ||
if not self.browser_options['headless']: | ||
timeout = 60000 | ||
await self.page.goto('https://www.' + self.cli.config['main']['domain'] + '/signin?redirectUrl=' + redirect_url) | ||
await self.page.type('#SignIn-emailInput', self.cli.username) | ||
await self.page.type('#SignIn-passwordInput', self.cli.password) | ||
await self.page.keyboard.press('Enter') | ||
try: | ||
await self.page.waitForNavigation(timeout=timeout) | ||
except Exception: | ||
return False | ||
|
||
if contains is not None: | ||
html = await self.page.content() | ||
for item in contains: | ||
if item not in html: | ||
return False | ||
|
||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import configparser | ||
import os | ||
import click | ||
import time | ||
from rich.console import Console | ||
from rich.panel import Panel | ||
from rich import box | ||
from KrogerAPI import * | ||
|
||
|
||
class KrogerCLI: | ||
config_file = 'config.ini' | ||
|
||
def __init__(self): | ||
self.config = configparser.ConfigParser() | ||
self.username = None | ||
self.password = None | ||
self.console = Console() | ||
self.api = KrogerAPI(self) | ||
if not os.path.exists(self.config_file): | ||
self._init_config_file() | ||
self.config.read(self.config_file) | ||
self.init() | ||
|
||
def init(self): | ||
if self.config['profile']['first_name'] != '': | ||
self.console.print(Panel('[bold]Welcome Back, ' + self.config['profile']['first_name'] + '! :smiley:\n' | ||
'[dark_blue]Kroger[/dark_blue] CLI[/bold]', box=box.ASCII)) | ||
else: | ||
self.console.print(Panel('[bold]Welcome to [dark_blue]Kroger[/dark_blue] CLI[/bold] (unofficial command ' | ||
'line interface)', box=box.ASCII)) | ||
|
||
self.prompt_store_selection() | ||
|
||
if self.username is None and self.config['main']['username'] != '': | ||
self.username = self.config['main']['username'] | ||
self.password = self.config['main']['password'] | ||
else: | ||
self.prompt_credentials() | ||
|
||
self.prompt_options() | ||
|
||
def prompt_store_selection(self): | ||
pass | ||
# TODO: | ||
# self.console.print('Please select preferred store') | ||
|
||
def prompt_credentials(self): | ||
self.console.print('In order to continue, please enter your username (email) and password for kroger.com ' | ||
'(also works with Ralphs, Dillons, Smith’s and other Kroger’s Chains)') | ||
username = click.prompt('Username (email)') | ||
password = click.prompt('Password') | ||
self._set_credentials(username, password) | ||
|
||
def prompt_options(self): | ||
while True: | ||
self.console.print('[bold]1[/bold] - Display account info') | ||
self.console.print('[bold]2[/bold] - Clip all digital coupons') | ||
self.console.print('[bold]8[/bold] - Re-Enter username/password') | ||
self.console.print('[bold]9[/bold] - Exit') | ||
option = click.prompt('Please select from one of the options', type=int) | ||
|
||
if option == 1: | ||
self._option_account_info() | ||
elif option == 2: | ||
self._option_clip_coupons() | ||
elif option == 8: | ||
self.prompt_credentials() | ||
elif option == 9: | ||
return | ||
|
||
self.console.rule() | ||
time.sleep(2) | ||
|
||
def _write_config_file(self): | ||
with open(self.config_file, 'w') as f: | ||
self.config.write(f) | ||
|
||
def _init_config_file(self): | ||
self.config.add_section('main') | ||
self.config['main']['username'] = '' | ||
self.config['main']['password'] = '' | ||
self.config['main']['domain'] = 'kroger.com' | ||
self.config.add_section('profile') | ||
self.config['profile']['first_name'] = '' | ||
self._write_config_file() | ||
|
||
def _set_credentials(self, username, password): | ||
self.username = username | ||
self.password = password | ||
self.config['main']['username'] = self.username | ||
self.config['main']['password'] = self.password | ||
self._write_config_file() | ||
|
||
def _option_account_info(self): | ||
info = self.api.get_account_info() | ||
if info is None: | ||
self.console.print('[bold red]Couldn\'t retrieve the account info.[/bold red]') | ||
else: | ||
self.config['profile']['first_name'] = info['firstName'] | ||
self.config['profile']['last_name'] = info['lastName'] | ||
self.config['profile']['email_address'] = info['emailAddress'] | ||
self.config['profile']['loyalty_card_number'] = info['loyaltyCardNumber'] | ||
self.config['profile']['mobile_phone'] = info['mobilePhoneNumber'] | ||
self.config['profile']['address_line1'] = info['address']['addressLine1'] | ||
self.config['profile']['address_line2'] = info['address']['addressLine2'] | ||
self.config['profile']['city'] = info['address']['city'] | ||
self.config['profile']['state'] = info['address']['stateCode'] | ||
self.config['profile']['zip'] = info['address']['zip'] | ||
self._write_config_file() | ||
self.console.print(self.config.items(section='profile')) | ||
|
||
def _option_clip_coupons(self): | ||
self.api.clip_coupons() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
call venv\scripts\activate | ||
|
||
pyinstaller -n kroger-cli ^ | ||
--onefile ^ | ||
--exclude-module tkinter ^ | ||
--hidden-import=six ^ | ||
--hidden-import=packaging ^ | ||
--hidden-import=packaging.version ^ | ||
--hidden-import=packaging.requirements ^ | ||
--hidden-import=packaging.specifiers ^ | ||
--hidden-import=pkg_resources ^ | ||
--hidden-import pkg_resources.py2_warn ^ | ||
main.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from KrogerCLI import * | ||
|
||
if __name__ == '__main__': | ||
cli = KrogerCLI() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
altgraph==0.17 | ||
appdirs==1.4.4 | ||
click==7.1.2 | ||
colorama==0.4.3 | ||
commonmark==0.9.1 | ||
future==0.18.2 | ||
packaging==20.3 | ||
pefile==2019.4.18 | ||
pprintpp==0.4.0 | ||
pyee==7.0.2 | ||
Pygments==2.6.1 | ||
pyinstaller @ https://github.com/pyinstaller/pyinstaller/archive/develop.zip | ||
pyparsing==2.4.7 | ||
pyppeteer==0.2.2 | ||
pywin32-ctypes==0.2.0 | ||
rich==1.1.3 | ||
six==1.14.0 | ||
tqdm==4.46.0 | ||
typing-extensions==3.7.4.2 | ||
urllib3==1.25.9 | ||
websockets==8.1 |