mirror of
https://github.com/zoffline/zwift-offline.git
synced 2026-08-02 17:08:14 -07:00
@@ -0,0 +1,44 @@
|
||||
{% extends "./layout.html" %}
|
||||
{% block content %}
|
||||
<h1><div class="text-shadow">Download Zwift profile</div></h1>
|
||||
<h2 class="text-shadow">Logged in as {{ username }} </h2>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-5">
|
||||
<form id="profile" action="{{ url_for('profile', username=username) }}" method="post">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="col-form-label col-form-label-sm text-shadow">Zwift Username</label>
|
||||
<input type="text" id="username" name="username" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="col-form-label col-form-label-sm text-shadow">Zwift Password</label>
|
||||
<input type="password" id="password" name="password" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-12 top-buffer">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="safe_zwift" name="safe_zwift">
|
||||
<label class="form-check-label text-shadow" for="safe_zwift">Save credentials (will automatically update activities to Zwift)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 top-buffer">
|
||||
<input type="submit" value="Submit" class="btn btn-sm btn-light">
|
||||
<a href="{{ url_for('user_home', username=username) }}" class="btn btn-sm btn-secondary">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul class="list-group top-buffer">
|
||||
{% for message in messages %}
|
||||
<li class="list-group-item py-2">
|
||||
<div class="text-shadow">{{ message }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends "./layout.html" %}
|
||||
{% block content %}
|
||||
<h1><div class="text-shadow">Zwift Launcher</div></h1>
|
||||
{% if username %}
|
||||
<h2 class="text-shadow">Logged in as {{ username }}</h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-5">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul class="list-group top-buffer">
|
||||
{% for message in messages %}
|
||||
<li class="list-group-item">
|
||||
<div class="text-shadow">{{ message }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -2,14 +2,15 @@
|
||||
{% block content %}
|
||||
<h1><div class="text-shadow">Logged in as {{ username }} ({{ name }})</div></h1>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-6">
|
||||
<div class="col-sm-8 col-md-7">
|
||||
<ul class="list-group">
|
||||
{% if profile %}
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="text-shadow">profile.bin - {{ profile }}</label>
|
||||
<a href="/download/profile.bin"><div class="btn btn-sm btn-light">Download</div></a>
|
||||
<table width="420px"><td><label class="text-shadow">profile.bin - {{ profile }}</label></td>
|
||||
<td align="right"><a href="/download/profile.bin"><div class="btn btn-sm btn-light">Download</div></a></td>
|
||||
<td align="right"><a href="/delete/profile.bin"><div class="btn btn-sm btn-light">Delete</div></a></td></table>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -18,7 +19,8 @@
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="text-shadow">strava_token.txt - {{ token }}</label>
|
||||
<table width="420px"><td><label class="text-shadow">strava_token.txt - {{ token }}</label></td>
|
||||
<td align="right"><a href="/delete/strava_token.txt"><div class="btn btn-sm btn-light">Delete</div></a></td></table>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -27,7 +29,18 @@
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="text-shadow">garmin_credentials.txt - {{ garmin }}</label>
|
||||
<table width="420px"><td><label class="text-shadow">garmin_credentials.txt - {{ garmin }}</label></td>
|
||||
<td align="right"><a href="/delete/garmin_credentials.txt"><div class="btn btn-sm btn-light">Delete</div></a></td></table>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if zwift %}
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table width="420px"><td><label class="text-shadow">zwift_credentials.txt - {{ zwift }}</label></td>
|
||||
<td align="right"><a href="/delete/zwift_credentials.txt"><div class="btn btn-sm btn-light">Delete</div></a></td></table>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -36,13 +49,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-5">
|
||||
<div class="col-sm-8 col-md-7">
|
||||
<form method="POST" action="/upload/{{ username }}/" enctype="multipart/form-data" class="top-buffer">
|
||||
<h3 class="text-shadow">Upload file</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="file" name="file" class="form-control-file text-shadow-not-ie" />
|
||||
<label class="text-shadow">(profile.bin / strava_token.txt / garmin_credentials.txt)</label>
|
||||
<label class="text-shadow">(profile.bin / strava_token.txt / garmin_credentials.txt / zwift_credentials.txt)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
<div class="col-md-12">
|
||||
<a href="{{ url_for('upload', username=username) }}" class="btn btn-sm btn-success">Upload</a>
|
||||
<a href="{{ url_for('reset', username=username) }}" class="btn btn-sm btn-secondary">Change password</a>
|
||||
<a href="{{ url_for('strava') }}" target="_blank" class="btn btn-sm btn-secondary">Strava auth</a>
|
||||
{% if server_ip %}
|
||||
<a href="{{ url_for('profile', username=username) }}" class="btn btn-sm btn-secondary">Get Zwift profile</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('logout', username=username) }}" class="btn btn-sm btn-secondary">Logout</a>
|
||||
{% if is_admin and not restarting %}
|
||||
<a href="/restart" class="btn btn-sm btn-danger">Restart server</a>
|
||||
@@ -62,6 +66,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul class="list-group top-buffer">
|
||||
{% for message in messages %}
|
||||
<li class="list-group-item">
|
||||
<div class="text-shadow">{{ message }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Executable
+165
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#
|
||||
# Adapted from https://github.com/jlemon/zlogger/blob/master/get_riders.py
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Jonathan Lemon
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import json
|
||||
import requests
|
||||
import protobuf.activity_pb2 as activity_pb2
|
||||
import protobuf.profile_pb2 as profile_pb2
|
||||
|
||||
|
||||
def post_credentials(session, username, password):
|
||||
# Credentials POSTing and tokens retrieval
|
||||
# POST https://secure.zwift.com/auth/realms/zwift/tokens/access/codes
|
||||
|
||||
try:
|
||||
response = session.post(
|
||||
url="https://secure.zwift.com/auth/realms/zwift/tokens/access/codes",
|
||||
headers={
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Host": "secure.zwift.com",
|
||||
"User-Agent": "Zwift/1.5 (iPhone; iOS 9.0.2; Scale/2.00)",
|
||||
"Accept-Language": "en-US;q=1",
|
||||
},
|
||||
data={
|
||||
"client_id": "Zwift_Mobile_Link",
|
||||
"username": username,
|
||||
"password": password,
|
||||
"grant_type": "password",
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
|
||||
json_dict = json.loads(response.content)
|
||||
|
||||
return (json_dict["access_token"], json_dict["refresh_token"], json_dict["expires_in"])
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print('HTTP Request failed: %s' % e)
|
||||
flash('HTTP Request failed: %s' % e)
|
||||
|
||||
except KeyError as e:
|
||||
print('Invalid uname and/or password')
|
||||
flash('Invalid uname and/or password')
|
||||
|
||||
|
||||
def query_player_profile(session, access_token):
|
||||
# Query Player Profile
|
||||
# GET https://us-or-rly101.zwift.com/api/profiles/<player_id>
|
||||
try:
|
||||
response = session.get(
|
||||
url="https://us-or-rly101.zwift.com/api/profiles/me",
|
||||
headers={
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept": "application/x-protobuf-lite",
|
||||
"Connection": "keep-alive",
|
||||
"Host": "us-or-rly101.zwift.com",
|
||||
"User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0",
|
||||
"Authorization": "Bearer %s" % access_token,
|
||||
"Accept-Language": "en-us",
|
||||
},
|
||||
)
|
||||
|
||||
return response.content
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print('HTTP Request failed: %s' % e)
|
||||
|
||||
|
||||
def logout(session, refresh_token):
|
||||
# Logout
|
||||
# POST https://secure.zwift.com/auth/realms/zwift/tokens/logout
|
||||
try:
|
||||
response = session.post(
|
||||
url="https://secure.zwift.com/auth/realms/zwift/tokens/logout",
|
||||
headers={
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Host": "secure.zwift.com",
|
||||
"User-Agent": "Zwift/1.5 (iPhone; iOS 9.0.2; Scale/2.00)",
|
||||
"Accept-Language": "en-US;q=1",
|
||||
},
|
||||
data={
|
||||
"client_id": "Zwift_Mobile_Link",
|
||||
"refresh_token": refresh_token,
|
||||
},
|
||||
)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print('HTTP Request failed: %s' % e)
|
||||
|
||||
|
||||
def login(session, user, password):
|
||||
access_token, refresh_token, expired_in = post_credentials(session, user, password)
|
||||
return access_token, refresh_token
|
||||
|
||||
def upload_activity(session, access_token, activity):
|
||||
try:
|
||||
response = session.put(
|
||||
url="https://us-or-rly101.zwift.com/api/profiles/%s/activities/%s" % (activity.player_id, activity.id),
|
||||
headers={
|
||||
"Content-Type": "application/x-protobuf-lite",
|
||||
"Accept": "application/json",
|
||||
"Connection": "keep-alive",
|
||||
"Host": "us-or-rly101.zwift.com",
|
||||
"User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0",
|
||||
"Authorization": "Bearer %s" % access_token,
|
||||
"Accept-Language": "en-us",
|
||||
},
|
||||
data=activity.SerializeToString(),
|
||||
)
|
||||
return response.status_code
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print('HTTP Request failed: %s' % e)
|
||||
|
||||
|
||||
def get_player_id(session, access_token):
|
||||
try:
|
||||
response = session.get(
|
||||
url="https://us-or-rly101.zwift.com/api/profiles/me",
|
||||
headers={
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept": "application/x-protobuf-lite",
|
||||
"Connection": "keep-alive",
|
||||
"Host": "us-or-rly101.zwift.com",
|
||||
"User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0",
|
||||
"Authorization": "Bearer %s" % access_token,
|
||||
"Accept-Language": "en-us",
|
||||
},
|
||||
)
|
||||
|
||||
profile = profile_pb2.Profile()
|
||||
profile.ParseFromString(response.content)
|
||||
return profile.id
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print('HTTP Request failed: %s' % e)
|
||||
+180
-13
@@ -14,6 +14,7 @@ import math
|
||||
import threading
|
||||
import re
|
||||
import smtplib, ssl
|
||||
import requests
|
||||
from copy import copy
|
||||
from functools import wraps
|
||||
from io import BytesIO
|
||||
@@ -43,6 +44,7 @@ import protobuf.world_pb2 as world_pb2
|
||||
import protobuf.zfiles_pb2 as zfiles_pb2
|
||||
import protobuf.hash_seeds_pb2 as hash_seeds_pb2
|
||||
import protobuf.events_pb2 as events_pb2
|
||||
import online_sync
|
||||
|
||||
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
||||
logger = logging.getLogger('zoffline')
|
||||
@@ -92,7 +94,7 @@ else:
|
||||
SECRET_KEY_FILE = "%s/secret-key.txt" % STORAGE_DIR
|
||||
ENABLEGHOSTS_FILE = "%s/enable_ghosts.txt" % STORAGE_DIR
|
||||
MULTIPLAYER = False
|
||||
garmin_key = None
|
||||
credentials_key = None
|
||||
if os.path.exists("%s/multiplayer.txt" % STORAGE_DIR):
|
||||
MULTIPLAYER = True
|
||||
try:
|
||||
@@ -111,12 +113,20 @@ if os.path.exists("%s/multiplayer.txt" % STORAGE_DIR):
|
||||
logger.warn("cryptography is not installed. Uploaded garmin_credentials.txt will not be encrypted.")
|
||||
encrypt = False
|
||||
if encrypt:
|
||||
GARMIN_KEY_FILE = "%s/garmin-key.txt" % STORAGE_DIR
|
||||
if not os.path.exists(GARMIN_KEY_FILE):
|
||||
with open(GARMIN_KEY_FILE, 'wb') as f:
|
||||
CREDENTIALS_KEY_FILE = "%s/credentials-key.txt" % STORAGE_DIR
|
||||
if not os.path.exists(CREDENTIALS_KEY_FILE):
|
||||
with open(CREDENTIALS_KEY_FILE, 'wb') as f:
|
||||
f.write(Fernet.generate_key())
|
||||
with open(GARMIN_KEY_FILE, 'rb') as f:
|
||||
garmin_key = f.read()
|
||||
with open(CREDENTIALS_KEY_FILE, 'rb') as f:
|
||||
credentials_key = f.read()
|
||||
|
||||
try:
|
||||
with open('%s/strava-client.txt' % STORAGE_DIR, 'r') as f:
|
||||
client_id = f.readline().rstrip('\r\n')
|
||||
client_secret = f.readline().rstrip('\r\n')
|
||||
except:
|
||||
client_id = '28117'
|
||||
client_secret = '41b7b7b76d8cfc5dc12ad5f020adfea17da35468'
|
||||
|
||||
from tokens import *
|
||||
|
||||
@@ -457,11 +467,93 @@ def reset(username):
|
||||
return render_template("reset.html", username=current_user.username)
|
||||
|
||||
|
||||
@app.route("/strava", methods=['GET'])
|
||||
@login_required
|
||||
def strava():
|
||||
try:
|
||||
from stravalib.client import Client
|
||||
except ImportError:
|
||||
flash("stravalib is not installed. Skipping Strava authorization attempt.")
|
||||
return redirect('/user/%s/' % current_user.username)
|
||||
client = Client()
|
||||
url = client.authorization_url(client_id=client_id,
|
||||
redirect_uri='https://%s/authorization' % server_ip,
|
||||
scope='activity:write')
|
||||
return redirect(url)
|
||||
|
||||
|
||||
@app.route("/authorization", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def authorization():
|
||||
from stravalib.client import Client
|
||||
try:
|
||||
client = Client()
|
||||
code = request.args.get('code')
|
||||
token_response = client.exchange_code_for_token(client_id=client_id, client_secret=client_secret, code=code)
|
||||
with open('%s/strava_token.txt' % os.path.join(STORAGE_DIR, str(current_user.player_id)), 'w') as f:
|
||||
f.write(client_id + '\n');
|
||||
f.write(client_secret + '\n');
|
||||
f.write(token_response['access_token'] + '\n');
|
||||
f.write(token_response['refresh_token'] + '\n');
|
||||
f.write(str(token_response['expires_at']) + '\n');
|
||||
flash("Strava authorized. Go to \"Upload\" to remove authorization.")
|
||||
except:
|
||||
flash("Strava canceled.")
|
||||
flash("Please close this window and return to Zwift Launcher.")
|
||||
return render_template("strava.html", username=current_user.username)
|
||||
|
||||
|
||||
@app.route("/profile/<username>/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def profile(username):
|
||||
if request.method == "POST":
|
||||
if request.form['username'] == "" or request.form['password'] == "":
|
||||
flash("Zwift credentials can't be empty")
|
||||
return render_template("profile.html", username=current_user.username)
|
||||
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
player_id = current_user.player_id
|
||||
profile_dir = '%s/%s' % (STORAGE_DIR, str(player_id))
|
||||
session = requests.session()
|
||||
|
||||
try:
|
||||
access_token, refresh_token = online_sync.login(session, username, password)
|
||||
try:
|
||||
profile = online_sync.query_player_profile(session, access_token)
|
||||
with open('%s/profile.bin' % SCRIPT_DIR, 'wb') as f:
|
||||
f.write(profile)
|
||||
online_sync.logout(session, refresh_token)
|
||||
os.rename('%s/profile.bin' % SCRIPT_DIR, '%s/profile.bin' % profile_dir)
|
||||
flash("Zwift profile installed locally.")
|
||||
except:
|
||||
flash("Error downloading profile")
|
||||
if request.form.get("safe_zwift", None) != None:
|
||||
try:
|
||||
file_path = os.path.join(profile_dir, 'zwift_credentials.txt')
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(username + '\n');
|
||||
f.write(password + '\n');
|
||||
if credentials_key is not None:
|
||||
with open(file_path, 'rb') as fr:
|
||||
zwift_credentials = fr.read()
|
||||
cipher_suite = Fernet(credentials_key)
|
||||
ciphered_text = cipher_suite.encrypt(zwift_credentials)
|
||||
with open(file_path, 'wb') as fw:
|
||||
fw.write(ciphered_text)
|
||||
flash("Zwift credentials saved")
|
||||
except:
|
||||
flash("Error saving 'zwift_credentiasl.txt' file")
|
||||
except:
|
||||
flash("Error invalid Username or password")
|
||||
return render_template("profile.html", username=current_user.username)
|
||||
|
||||
|
||||
@app.route("/user/<username>/")
|
||||
@login_required
|
||||
def user_home(username):
|
||||
return render_template("user_home.html", username=current_user.username, enable_ghosts=bool(current_user.enable_ghosts),
|
||||
online=get_online(), is_admin=current_user.is_admin, restarting=restarting, restarting_in_minutes=restarting_in_minutes)
|
||||
online=get_online(), is_admin=current_user.is_admin, restarting=restarting, restarting_in_minutes=restarting_in_minutes, server_ip=os.path.exists(SERVER_IP_FILE))
|
||||
|
||||
|
||||
def send_message_to_all_online(message, sender='Server'):
|
||||
@@ -556,16 +648,23 @@ def upload(username):
|
||||
|
||||
if request.method == 'POST':
|
||||
uploaded_file = request.files['file']
|
||||
if uploaded_file.filename in ['profile.bin', 'strava_token.txt', 'garmin_credentials.txt']:
|
||||
if uploaded_file.filename in ['profile.bin', 'strava_token.txt', 'garmin_credentials.txt', 'zwift_credentials.txt']:
|
||||
file_path = os.path.join(profile_dir, uploaded_file.filename)
|
||||
uploaded_file.save(file_path)
|
||||
if uploaded_file.filename == 'garmin_credentials.txt' and garmin_key is not None:
|
||||
if uploaded_file.filename == 'garmin_credentials.txt' and credentials_key is not None:
|
||||
with open(file_path, 'rb') as fr:
|
||||
garmin_credentials = fr.read()
|
||||
cipher_suite = Fernet(garmin_key)
|
||||
cipher_suite = Fernet(credentials_key)
|
||||
ciphered_text = cipher_suite.encrypt(garmin_credentials)
|
||||
with open(file_path, 'wb') as fw:
|
||||
fw.write(ciphered_text)
|
||||
if uploaded_file.filename == 'zwift_credentials.txt' and credentials_key is not None:
|
||||
with open(file_path, 'rb') as fr:
|
||||
garmin_credentials = fr.read()
|
||||
cipher_suite = Fernet(credentials_key)
|
||||
ciphered_text = cipher_suite.encrypt(garmin_credentials)
|
||||
with open(file_path, 'wb') as fw:
|
||||
fw.write(ciphered_text)
|
||||
flash("File %s uploaded." % uploaded_file.filename)
|
||||
else:
|
||||
flash("Invalid file name.")
|
||||
@@ -590,8 +689,13 @@ def upload(username):
|
||||
if os.path.isfile(garmin_file):
|
||||
stat = os.stat(garmin_file)
|
||||
garmin = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime))
|
||||
zwift = None
|
||||
zwift_file = os.path.join(profile_dir, 'zwift_credentials.txt')
|
||||
if os.path.isfile(zwift_file):
|
||||
stat = os.stat(zwift_file)
|
||||
zwift = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime))
|
||||
|
||||
return render_template("upload.html", username=current_user.username, profile=profile, name=name, token=token, garmin=garmin)
|
||||
return render_template("upload.html", username=current_user.username, profile=profile, name=name, token=token, garmin=garmin, zwift=zwift)
|
||||
|
||||
|
||||
@app.route("/download/profile.bin", methods=["GET"])
|
||||
@@ -604,6 +708,17 @@ def download():
|
||||
return send_file(profile_file, attachment_filename='profile.bin')
|
||||
|
||||
|
||||
@app.route("/delete/<filename>", methods=["GET"])
|
||||
@login_required
|
||||
def delete(filename):
|
||||
player_id = current_user.player_id
|
||||
profile_dir = os.path.join(STORAGE_DIR, str(player_id))
|
||||
delete_file = os.path.join(profile_dir, filename)
|
||||
if os.path.isfile(delete_file):
|
||||
os.remove("%s" %delete_file)
|
||||
return redirect(url_for('upload', username=current_user))
|
||||
|
||||
|
||||
@app.route("/logout/<username>")
|
||||
@login_required
|
||||
def logout(username):
|
||||
@@ -1040,8 +1155,8 @@ def garmin_upload(player_id, activity):
|
||||
profile_dir = '%s/%s' % (STORAGE_DIR, player_id)
|
||||
try:
|
||||
with open('%s/garmin_credentials.txt' % profile_dir, 'r') as f:
|
||||
if garmin_key is not None:
|
||||
cipher_suite = Fernet(garmin_key)
|
||||
if credentials_key is not None:
|
||||
cipher_suite = Fernet(credentials_key)
|
||||
ciphered_text = f.read()
|
||||
unciphered_text = (cipher_suite.decrypt(ciphered_text.encode(encoding='UTF-8')))
|
||||
unciphered_text = unciphered_text.decode(encoding='UTF-8')
|
||||
@@ -1067,6 +1182,57 @@ def garmin_upload(player_id, activity):
|
||||
logger.warn("Garmin upload failed. No internet?")
|
||||
|
||||
|
||||
def zwift_upload(player_id):
|
||||
profile_dir = '%s/%s' % (STORAGE_DIR, player_id)
|
||||
SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR
|
||||
if not os.path.exists(SERVER_IP_FILE):
|
||||
logger.info("server_ip.txt missing, skip Zwift activity update")
|
||||
return
|
||||
try:
|
||||
with open('%s/zwift_credentials.txt' % profile_dir, 'r') as f:
|
||||
if credentials_key is not None:
|
||||
cipher_suite = Fernet(credentials_key)
|
||||
ciphered_text = f.read()
|
||||
unciphered_text = (cipher_suite.decrypt(ciphered_text.encode(encoding='UTF-8')))
|
||||
unciphered_text = unciphered_text.decode(encoding='UTF-8')
|
||||
split_credentials = unciphered_text.splitlines()
|
||||
username = split_credentials[0]
|
||||
password = split_credentials[1]
|
||||
else:
|
||||
username = f.readline().rstrip('\r\n')
|
||||
password = f.readline().rstrip('\r\n')
|
||||
except:
|
||||
logger.warn("Failed to read %s/zwift_credentials.txt. Skipping Zwift upload attempt." % profile_dir)
|
||||
return
|
||||
|
||||
try:
|
||||
session = requests.session()
|
||||
try:
|
||||
activity = activity_pb2.Activity()
|
||||
access_token, refresh_token = online_sync.login(session, username, password)
|
||||
activity.player_id = online_sync.get_player_id(session, access_token)
|
||||
player_id = current_user.player_id
|
||||
profile_dir = '%s/%s' % (STORAGE_DIR, str(player_id))
|
||||
activity_file = '%s/last_activity.bin' % profile_dir
|
||||
if not os.path.isfile(activity_file):
|
||||
print('Activity file not found')
|
||||
with open(activity_file, 'rb') as fd:
|
||||
try:
|
||||
activity.ParseFromString(fd.read())
|
||||
except:
|
||||
print('Could not parse activity file')
|
||||
res = online_sync.upload_activity(session, access_token, activity)
|
||||
if res == 200:
|
||||
logger.info("Zwift activity upload succesfull")
|
||||
else:
|
||||
logger.warn("Zwift activity upload failed:%s:" %res)
|
||||
online_sync.logout(session, refresh_token)
|
||||
except:
|
||||
logger.warn("Error uploading activity to Zwift Server")
|
||||
except:
|
||||
logger.warn("Zwift upload failed. No internet?")
|
||||
|
||||
|
||||
# With 64 bit ids Zwift can pass negative numbers due to overflow, which the flask int
|
||||
# converter does not handle so it's a string argument
|
||||
@app.route('/api/profiles/<int:player_id>/activities/<string:activity_id>', methods=['PUT'])
|
||||
@@ -1099,6 +1265,7 @@ def api_profiles_activities_id(player_id, activity_id):
|
||||
# For using with upload_activity.py (to upload zoffline activity to Zwift server)
|
||||
with open('%s/%s/last_activity.bin' % (STORAGE_DIR, player_id), 'wb') as f:
|
||||
f.write(activity.SerializeToString())
|
||||
zwift_upload(player_id)
|
||||
return response, 200
|
||||
|
||||
@app.route('/api/profiles/<int:recieving_player_id>/activities/0/rideon', methods=['POST']) #activity_id Seem to always be 0, even when giving ride on to ppl with 30km+
|
||||
|
||||
Reference in New Issue
Block a user