#-*- coding: utf8 -*- #! /usr/bin/python3 # 7-Eleven Python implementation. This program allows you to lock in a fuel price from your computer. # Copyright (C) 2019 Freyta # # 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 . from flask import Flask, render_template, request, redirect, url_for, session, flash # Optional Security # Uncomment Basic Auth section to enable basic authentication so users will be prompted a username and password before seeing the website. #from flask_basicauth import BasicAuth # Uncomment SSL section to enable HTTPS (certificates required) #import ssl #context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) #context.load_cert_chain('domain.crt', 'domain.key') # Used for getting/settings the OS environments and for writing/reading the stores.json file import sys, os, time # Used for sending requests to 7-Eleven and getting the response in a JSON format. import requests, json # Used for our randomly generated Device ID (if needed) import random # Used to set our coordinates while locking in a price import googlemaps # Used for all of our custom settings and functions import settings, functions # Used for the auto locking function from apscheduler.schedulers.background import BackgroundScheduler # Used to load and save details from the autolock.ini config file and import our autolocker import autolocker, configparser # If we haven't set the API key or it is it's default value, warn the user that we will disable the Google Maps search. if(functions.API_KEY in [None,"changethis",""]): custom_coords = False print("Note: You have not set an API key. You will not be able to use Google to find a stores coordinates.\nBut you can still use the manual search if you know the postcode to the store you want to lock in from.\n\n\n\n\n") # If we have not set a device ID then let them know a random one will be generated if(os.getenv('DEVICE_ID', settings.DEVICE_ID) in [None,"changethis",""]): print("Note: You have not set a device ID. A random one will be set when you login.") app = Flask(__name__) # Uncomment to enable basic authentication #app.config['BASIC_AUTH_FORCE'] = True #app.config['BASIC_AUTH_USERNAME'] = 'petrol' #app.config['BASIC_AUTH_PASSWORD'] = 'lockit' #basic_auth = BasicAuth(app) @app.route('/') def index(): # If they have pressed the refresh link remove the error and success messages if(request.args.get('action') == "refresh"): session.pop('ErrorMessage', None) session.pop('SuccessMessage', None) session.pop('fuelType', None) session.pop('LockinPrice', None) try: # Regenerate our locked in prices functions.lockedPrices() except: # If there was an error, lets just move on. pass # If the environmental variable DEVICE_ID is empty or is not set at all if(os.getenv('DEVICE_ID', settings.DEVICE_ID) in [None,"changethis",""]): # Set the device id to a randomly generated one DEVICE_ID = ''.join(random.choice('0123456789abcdef') for i in range(16)) else: # Otherwise we set the it to the one set in settings.py DEVICE_ID = os.getenv('DEVICE_ID', settings.DEVICE_ID) # Set the session max price for the auto locker session['max_price'] = config['General']['max_price'] # Get the cheapest fuel price to show on the automatic lock in page fuelPrice = functions.cheapestFuelAll() return render_template('price.html',device_id=DEVICE_ID) @app.route('/login', methods=['POST', 'GET']) def login(): # Clear the error and success message session.pop('ErrorMessage', None) session.pop('SuccessMessage', None) session.pop('fuelType', None) if request.method == 'POST': # If the device ID field was left blank, set a random one if ((request.form['device_id']) in [None,""]): session['DEVICE_ID'] = os.getenv('DEVICE_ID', ''.join(random.choice('0123456789abcdef') for i in range(16))) else: # Since it was filled out, we will use that for the rest of the session session['DEVICE_ID'] = os.getenv('DEVICE_ID', request.form['device_id']) # Get the email and password from the login form password = str(request.form['password']) email = str(request.form['email']) # The payload that we use to login payload = '{"Email":"' + email + '","Password":"' + password + '","DeviceName":"' + functions.DEVICE_NAME + '","DeviceOsNameVersion":"' + functions.OS_VERSION +'"}' # Generate the tssa string tssa = functions.generateTssa(functions.BASE_URL + "account/login", "POST", payload) # Assign the headers headers = {'User-Agent':'Apache-HttpClient/UNAVAILABLE (java 1.4)', 'Authorization':'%s' % tssa, 'X-OsVersion':functions.OS_VERSION, 'X-OsName':'Android', 'X-DeviceID':session['DEVICE_ID'], 'X-VmobID':functions.des_encrypt_string(session['DEVICE_ID']), 'X-AppVersion':functions.APP_VERSION, 'Content-Type':'application/json; charset=utf-8'} # Login now! response = requests.post(functions.BASE_URL + "account/login", data=payload, headers=headers) returnHeaders = response.headers returnContent = json.loads(response.text) try: # If there was an error logging in, redirect to the index page with the 7Eleven response if(returnContent['Message']): session['ErrorMessage'] = returnContent['Message'] return redirect(url_for('index')) except: # We need the AccessToken from the response header accessToken = str(returnHeaders).split("'X-AccessToken': '") accessToken = accessToken[1].split("'") accessToken = accessToken[0] # DeviceSecretToken and accountID are both needed to lock in a fuel price deviceSecret = returnContent['DeviceSecretToken'] accountID = returnContent['AccountId'] # Save the users first name and their card balance so we can display it firstName = returnContent['FirstName'] cardBalance = str(returnContent['DigitalCard']['Balance']) session['deviceSecret'] = deviceSecret session['accessToken'] = accessToken session['accountID'] = accountID session['firstName'] = firstName session['cardBalance'] = cardBalance functions.lockedPrices() # If we have ticked enable auto lock in, then set boolean to true if(request.form.getlist('auto_lockin')): config.set('General', 'auto_lock_enabled', "True") session['auto_lock'] = True else: # We didn't want to save it, so set to false config.set('General', 'auto_lock_enabled', "False") session['auto_lock'] = False # Save their log in anyway, so we can update the auto lock option later if needed config.set('Account', 'deviceSecret', session['deviceSecret']) config.set('Account', 'accessToken', session['accessToken']) config.set('Account', 'cardBalance', session['cardBalance']) config.set('Account', 'account_ID', session['accountID']) config.set('Account', 'DEVICE_ID', session['DEVICE_ID']) # If we have an active fuel lock, set fuel_lock_saved to true, otherwise false if(session['fuelLockStatus'] == 0): config.set('Account', 'fuel_lock_saved', "True") else: config.set('Account', 'fuel_lock_saved', "False") # Write the config to file with open('./autolock.ini', 'w') as configfile: config.write(configfile) return redirect(url_for('index')) else: # They didn't submit a POST request, so we will redirect to index return redirect(url_for('index')) @app.route('/logout') def logout(): # The logout payload is an empty string but it is still needed payload = '""' tssa = functions.generateTssa(functions.BASE_URL + "account/logout", "POST", payload, session['accessToken']) headers = {'User-Agent':'Apache-HttpClient/UNAVAILABLE (java 1.4)', 'Authorization':'%s' % tssa, 'X-OsVersion':functions.OS_VERSION, 'X-OsName':'Android', 'X-DeviceID':session['DEVICE_ID'], 'X-VmobID':functions.des_encrypt_string(session['DEVICE_ID']), 'X-AppVersion':functions.APP_VERSION, 'X-DeviceSecret':session['deviceSecret'], 'Content-Type':'application/json; charset=utf-8'} response = requests.post(functions.BASE_URL + "account/logout", data=payload, headers=headers) # Clear all of the previously set session variables and then redirect to the index page session.clear() return redirect(url_for('index')) # The confirmation page for a manual lock in @app.route('/confirm') def confirm(): # See if we have a temporary lock in price try: if(session['LockinPrice']): # Since we do, show the confirmation page return render_template('confirm_price.html') except: # We haven't started locking in yet, show an error and redirect to the index session['ErrorMessage'] = "You haven't started the lock in process yet. Please try again." return redirect(url_for('index')) # Save the auto lock in settings @app.route('/save_settings', methods=['POST']) def save_settings(): # If we have sent the form if request.method == 'POST': # If we want to auto lock in, set it to true if(request.form.getlist('auto_lockin')): config.set('General', 'auto_lock_enabled', "True") config.set('General', 'auto_lock_fuel_type', request.form['fueltype']) session['auto_lock'] = True # Set the max price to what the user wants as long as it's above 80 and below 2 dollars if (float(request.form['max_price']) > 80 and float(request.form['max_price']) < 200): config.set('General', 'max_price', request.form['max_price']) else: session['ErrorMessage'] = "The price you tried to lock in was either too cheap or too expensive. It should be between 80 cents and 2 dollars" return redirect(url_for('index')) else: config.set('General', 'auto_lock_enabled', "False") session['auto_lock'] = False # Save the config file with open('./autolock.ini', 'w') as configfile: config.write(configfile) session['SuccessMessage'] = "Settings saved succesfully." return redirect(url_for('index')) @app.route('/lockin', methods=['POST', 'GET']) def lockin(): if request.method == 'POST': # Variable used to search for a manual price priceOveride = False # Get the form submission method submissionMethod = request.form['submit'] # If we didn't try to confirm a manual price if(submissionMethod != "confirm_price"): # Get the fuel type we want fuelType = str(request.form['fueltype']) session['fuelType'] = fuelType # Clear previous messages session.pop('ErrorMessage', None) session.pop('SuccessMessage', None) # Get the postcode and price of the cheapest fuel locationResult = functions.cheapestFuel(fuelType) # They tried to do something different from the manual and automatic form, so throw up an error if(submissionMethod != "manual" and submissionMethod != "automatic"): session['ErrorMessage'] = "Invalid form submission. Either use the manual or automatic one on the main page." return redirect(url_for('index')) # If they have manually chosen a postcode/suburb set the price overide to true if(submissionMethod == "manual"): postcode = str(request.form['postcode']) priceOveride = True # Get the latitude and longitude from the store storeLatLng = functions.getStoreAddress(postcode) # If we have a match, set the coordinates. If we don't, use Google Maps if (storeLatLng): locLat = float(storeLatLng[0]) locLat += (random.uniform(0.01,0.000001) * random.choice([-1,1])) locLong = float(storeLatLng[1]) locLong += (random.uniform(0.01,0.000001) * random.choice([-1,1])) else: # If we have entered the wrong manual postcode for a store, and haven't # set the Google Maps API, return an error since we cant use the API if not custom_coords: # If it is, get the error message and return back to the index session['ErrorMessage'] = "Error: You entered a manual postcode where no 7-Eleven store is. Please set a Google Maps API key or enter a valid postcode." return redirect(url_for('index')) # Initiate the Google Maps API gmaps = googlemaps.Client(key = API_KEY) # Get the longitude and latitude from the submitted postcode geocode_result = gmaps.geocode(str(request.form['postcode']) + ', Australia') locLat = geocode_result[0]['geometry']['location']['lat'] locLong = geocode_result[0]['geometry']['location']['lng'] else: # It was an automatic submission so we will now get the coordinates of the store # and add a random value to it so we don't appear to lock in from the service station locLat = locationResult[2] locLat += (random.uniform(0.01,0.000001) * random.choice([-1,1])) locLong = locationResult[3] locLong += (random.uniform(0.01,0.000001) * random.choice([-1,1])) # The payload to start the lock in process. payload = '{"LastStoreUpdateTimestamp":' + str(int(time.time())) + ',"Latitude":"' + str(locLat) + '","Longitude":"' + str(locLong) + '"}' tssa = functions.generateTssa(functions.BASE_URL + "FuelLock/StartSession", "POST", payload, session['accessToken']) # Now we start the request header headers = {'User-Agent':'Apache-HttpClient/UNAVAILABLE (java 1.4)', 'Authorization':'%s' % tssa, 'X-OsVersion':functions.OS_VERSION, 'X-OsName':'Android', 'X-DeviceID':session['DEVICE_ID'], 'X-VmobID':functions.des_encrypt_string(session['DEVICE_ID']), 'X-AppVersion':functions.APP_VERSION, 'X-DeviceSecret':session['deviceSecret'], 'Content-Type':'application/json; charset=utf-8'} # Send the request response = requests.post(functions.BASE_URL + "FuelLock/StartSession", data=payload, headers=headers) # Get the response content so we can check the fuel price returnContent = response.content # Move the response json into an array so we can read it returnContent = json.loads(returnContent) # If there is a fuel lock already in place we get an error! try: if returnContent['ErrorType'] == 0: session['ErrorMessage'] = "An error has occured. This is most likely due to a fuel lock already being in place." return redirect(url_for('index')) except: pass # Get the fuel price of all the types of fuel for each in returnContent['CheapestFuelTypeStores']: x = each['FuelPrices'] for i in x: if(str(i['Ean']) == fuelType): LockinPrice = i['Price'] session['LockinPrice'] = LockinPrice # If we have performed an automatic search we run the lowest price check # LockinPrice = the price from the 7/11 website # locationResult[1] = the price from the master131 script # If the price that we tried to lock in is more expensive than scripts price, we return an error if not(priceOveride): if not(float(LockinPrice) <= float(locationResult[1])): session['ErrorMessage'] = "The fuel price is too high compared to the cheapest available. The cheapest we found was at " + locationResult[0] + ". Try locking in there!" return redirect(url_for('index')) if(priceOveride): return redirect(url_for('confirm')) # Now we want to lock in the maximum litres we can. NumberOfLitres = int(150) # Lets start the actual lock in process payload = '{"AccountId":"' + session['accountID'] + '","FuelType":"' + session['fuelType'] + '","NumberOfLitres":"' + str(NumberOfLitres) + '"}' tssa = functions.generateTssa(functions.BASE_URL + "FuelLock/Confirm", "POST", payload, session['accessToken']) headers = {'User-Agent':'Apache-HttpClient/UNAVAILABLE (java 1.4)', 'Authorization':'%s' % tssa, 'X-OsVersion':functions.OS_VERSION, 'X-OsName':'Android', 'X-DeviceID':session['DEVICE_ID'], 'X-VmobID':functions.des_encrypt_string(session['DEVICE_ID']), 'X-AppVersion':functions.APP_VERSION, 'X-DeviceSecret':session['deviceSecret'], 'Content-Type':'application/json; charset=utf-8'} # Send through the request and get the response response = requests.post(functions.BASE_URL + "FuelLock/Confirm", data=payload, headers=headers) # Get the response into a json array returnContent = json.loads(response.content) try: # Check if the response was an error message if(returnContent['Message']): # If it is, get the error message and return back to the index session['ErrorMessage'] = returnContent['Message'] return redirect(url_for('index')) # Otherwise we most likely locked in the price! if(returnContent['Status'] == "0"): # Update the fuel prices that are locked in functions.lockedPrices() # Get amoount of litres that was locked in from the returned JSON array session['TotalLitres'] = returnContent['TotalLitres'] session['SuccessMessage'] = "The price was locked in for " + str(session['LockinPrice']) + " cents per litre" # Pop our fueltype and lock in price variables session.pop('fuelType', None) session.pop('LockinPrice', None) return redirect(url_for('index')) # For whatever reason it saved our lock in anyway and return to the index page except: # Update the fuel prices that are locked in functions.lockedPrices() session['SuccessMessage'] = "The price was locked in for " + str(session['LockinPrice']) + " cents per litre" # Get amoount of litres that was locked in from the returned JSON array session['TotalLitres'] = returnContent['TotalLitres'] # Pop our fueltype and lock in price variables session.pop('fuelType', None) session.pop('LockinPrice', None) return redirect(url_for('index')) else: # They just tried to load the lock in page without sending any data session['ErrorMessage'] = "Unknown error occured. Please try again!" return redirect(url_for('index')) if __name__ == '__main__': # Try and open stores.json if(os.path.isfile('./stores.json')): with open('./stores.json', 'r') as f: try: print("Note: Opening stores.json") stores = json.load(f) except: pass try: # Check to see if the stores.json file is older than 1 week. # If it is, we will download a new version if(stores['AsOfDate'] < (time.time() - (60 * 60 * 24 * 7))): print("Note: Your stores.json file is too old, updating it..") with open('./stores.json', 'wb') as f: f.write(functions.getStores()) print("Note: Updating stores.json complete") except: # Our json file isn't what we expected, so we will download a new one. with open('./stores.json', 'wb') as f: f.write(functions.getStores()) else: # We have no stores.json file, so we wil download it print("Note: No stores.json found, creating it for you.") with open('./stores.json', 'wb') as f: f.write(functions.getStores()) # Check if the autolock.ini file exists, if it doesn't create it. if not (os.path.exists("./autolock.ini")): autolocker.create_ini() # Open the config file and read the settings config = configparser.ConfigParser() config.read("./autolock.ini") # Start the autosearch scheduler if(functions.TZ in [None,""]): scheduler = BackgroundScheduler(timezone='UTC') else: scheduler = BackgroundScheduler(timezone=functions.TZ) # Start the price search thread and run it every 30 minutes scheduler.add_job(autolocker.start_lockin, 'interval', seconds=1800) scheduler.start() app.secret_key = os.urandom(12) app.run(host='0.0.0.0') # Uncomment to enable HTTPS/SSL and comment out the line above #app.run(host='0.0.0.0',port=443,ssl_context=context)