forked from freyta/7Eleven-Python
-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.py
498 lines (403 loc) · 22.3 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
#-*- 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 <https://www.gnu.org/licenses/>.
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)