Skip to content

Instantly share code, notes, and snippets.

@mbarnes
Last active July 20, 2023 13:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbarnes/a48a615c79fb1332677fd74e03e99a4a to your computer and use it in GitHub Desktop.
Save mbarnes/a48a615c79fb1332677fd74e03e99a4a to your computer and use it in GitHub Desktop.
Marc's Digital Coupon Clipper
#!/usr/bin/python3
#
# Clip all available digital coupons for Marc's grocery chain.
#
# This system does not use passwords, only your phone number!
#
import html.parser
import http
import json
import os
import re
import sys
import urllib
# 3rd-party modules
import requests
# Show HTTP requests and responses
http.client.HTTPConnection.debuglevel = 0
class FormExtractor(html.parser.HTMLParser):
def __init__(self, form_id=None, convert_charrefs=True):
super().__init__(convert_charrefs=convert_charrefs)
self.form_id = form_id
def reset(self):
self.__in_form = False
self.method = None
self.action = None
self.submit = {}
self.data = {}
super().reset()
def handle_starttag(self, tag, attrs):
attrs = {name: value for name, value in attrs}
if tag == 'form':
if not self.form_id or attrs.get('id') == self.form_id:
self.__in_form = True
self.method = attrs['method']
self.action = attrs['action']
elif tag == 'input' and self.__in_form:
if attrs.get('type') == 'submit':
self.submit[attrs['name']] = attrs['value']
else:
self.data[attrs['name']] = attrs.get('value', '')
def handle_endtag(self, tag):
if tag == 'form':
self.__in_form = False
def marcs_session_login(method):
def inner(session, *args, **kwargs):
"""Log in to Marcs.com on first call"""
if 'x-auth-token' not in session.headers:
url = 'https://www.marcs.com/My-Marcs-Login'
response = session.get(url)
response.raise_for_status()
form = FormExtractor()
form.feed(response.content.decode('utf-8'))
for key in form.data:
if key.endswith('USphone$txt1st'):
form.data[key] = session.phone_triplet[0]
if key.endswith('USphone$txt2nd'):
form.data[key] = session.phone_triplet[1]
if key.endswith('USphone$txt3rd'):
form.data[key] = session.phone_triplet[2]
for key in form.submit:
if key.endswith('SubmitButton'):
form.data[key] = form.submit[key]
response = session.post(url, data=form.data)
response.raise_for_status()
auth_token = session.cookies.get('inmarCookie', domain='www.marcs.com', path='/')
if auth_token:
session.headers['x-auth-token'] = auth_token
return method(session, *args, **kwargs)
return inner
class MarcsSession(requests.Session):
"""Marcs.com REST API session"""
base_url = 'https://ice.dpn.inmar.com/v2'
def __init__(self, phone_triplet, base_url=None):
if base_url:
self.base_url = base_url
super().__init__()
self.headers = {
'x-retailer': 'marcsstores'
}
self.phone_triplet = phone_triplet
self.loggedin = False
def request(self, method, url, *args, **kwargs):
"""Send the request after generating the complete URL"""
url = self.create_url(url)
return super().request(method, url, *args, **kwargs)
def create_url(self, url):
"""Create the URL based off this partial path"""
return urllib.parse.urljoin(self.base_url, url)
@marcs_session_login
def activate_offer(self, offer):
"""Activate a discount offer"""
url = '/offers/clip'
data = {
'id': offer['mdid']
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
response = self.post(url, data=data, headers=headers)
if response.status_code == requests.codes['conflict']:
return False
response.raise_for_status()
data = response.json()
return data['clipped']
@marcs_session_login
def available_offers(self):
"""List available discount offers"""
url = '/offers'
count = 0
params = { 'limit': 50 }
while True:
response = self.get(url, params=params)
response.raise_for_status()
offer_list = response.json()
if offer_list:
count += len(offer_list)
yield from offer_list
else:
break
params['skip'] = count
def main():
if len(sys.argv) != 2:
program_name = os.path.basename(sys.argv[0])
print('Usage:', program_name, 'PHONENUMBER', file=sys.stderr)
sys.exit(2)
phone_number = sys.argv[1]
try:
match = re.match('\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})', phone_number)
except re.error:
print('Unrecognized phone number format:', phone_number, file=sys.stderr)
sys.exit(2)
phone_triplet = (match.group(1), match.group(2), match.group(3))
with MarcsSession(phone_triplet) as session:
clipped = []
for offer in session.available_offers():
if not offer['clipped'] and session.activate_offer(offer):
print('CLIPPED:', offer['shortDescription'])
clipped.append(offer)
print(len(clipped), 'coupons clipped')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment