409 lines
14 KiB
Python
409 lines
14 KiB
Python
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
from __future__ import unicode_literals, absolute_import
|
|
|
|
# All the functionality needed to connect to Strava
|
|
from scipy import optimize
|
|
from scipy.signal import savgol_filter
|
|
import time
|
|
from time import strftime
|
|
|
|
|
|
import django_rq
|
|
queue = django_rq.get_queue('default')
|
|
queuelow = django_rq.get_queue('low')
|
|
queuehigh = django_rq.get_queue('low')
|
|
|
|
from rowers.dataprep import columndict
|
|
|
|
from rowers.rower_rules import is_workout_user,ispromember
|
|
|
|
import stravalib
|
|
from stravalib.exc import ActivityUploadFailed,TimeoutExceeded
|
|
|
|
from iso8601 import ParseError
|
|
from rowers.utils import myqueue
|
|
|
|
import rowers.mytypes as mytypes
|
|
import gzip
|
|
|
|
from rowers.tasks import handle_strava_sync,fetch_strava_workout
|
|
|
|
from rowsandall_app.settings import (
|
|
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
|
|
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
|
|
SITE_URL
|
|
)
|
|
|
|
try:
|
|
from json.decoder import JSONDecodeError
|
|
except ImportError: # pragma: no cover
|
|
JSONDecodeError = ValueError
|
|
|
|
from rowers.imports import *
|
|
from rowers.utils import dologging
|
|
|
|
webhookverification = "kudos_to_rowing"
|
|
webhooklink = SITE_URL+'/rowers/strava/webhooks/'
|
|
|
|
headers = {'Accept': 'application/json',
|
|
'Api-Key': STRAVA_CLIENT_ID,
|
|
'Content-Type': 'application/json',
|
|
'user-agent': 'sanderroosendaal'}
|
|
|
|
|
|
oauth_data = {
|
|
'client_id': STRAVA_CLIENT_ID,
|
|
'client_secret': STRAVA_CLIENT_SECRET,
|
|
'redirect_uri': STRAVA_REDIRECT_URI,
|
|
'autorization_uri': "https://www.strava.com/oauth/authorize",
|
|
'content_type': 'application/json',
|
|
'tokenname': 'stravatoken',
|
|
'refreshtokenname': 'stravarefreshtoken',
|
|
'expirydatename': 'stravatokenexpirydate',
|
|
'bearer_auth': True,
|
|
'base_url': "https://www.strava.com/oauth/token",
|
|
'grant_type': 'refresh_token',
|
|
'headers': headers,
|
|
'scope':'activity:write,activity:read_all',
|
|
}
|
|
|
|
|
|
# Exchange access code for long-lived access token
|
|
def get_token(code):
|
|
return imports_get_token(code, oauth_data)
|
|
|
|
def strava_open(user):
|
|
t = time.localtime()
|
|
timestamp = time.strftime('%b-%d-%Y_%H%M', t)
|
|
with open('strava_open.log','a') as f:
|
|
f.write('\n')
|
|
f.write(timestamp)
|
|
f.write(' ')
|
|
f.write('Getting token for user ')
|
|
f.write(str(user.rower.id))
|
|
f.write(' token expiry ')
|
|
f.write(str(user.rower.stravatokenexpirydate))
|
|
f.write(' ')
|
|
f.write(json.dumps(oauth_data))
|
|
f.write('\n')
|
|
token = imports_open(user, oauth_data)
|
|
if user.rower.strava_owner_id == 0: # pragma: no cover
|
|
strava_owner_id = set_strava_athlete_id(user)
|
|
return token
|
|
|
|
def do_refresh_token(refreshtoken):
|
|
return imports_do_refresh_token(refreshtoken, oauth_data)
|
|
|
|
def rower_strava_token_refresh(user):
|
|
r = Rower.objects.get(user=user)
|
|
res = do_refresh_token(r.stravarefreshtoken)
|
|
access_token = res[0]
|
|
expires_in = res[1]
|
|
refresh_token = res[2]
|
|
expirydatetime = timezone.now()+timedelta(seconds=expires_in)
|
|
|
|
r.stravatoken = access_token
|
|
r.stravatokenexpirydate = expirydatetime
|
|
r.stravarefreshtoken = refresh_token
|
|
r.save()
|
|
|
|
return r.stravatoken
|
|
|
|
# Make authorization URL including random string
|
|
def make_authorization_url(request): # pragma: no cover
|
|
return imports_make_authorization_url(oauth_data)
|
|
|
|
def strava_establish_push(): # pragma: no cover
|
|
url = "https://www.strava.com/api/v3/push_subscriptions"
|
|
post_data = {
|
|
'client_id': STRAVA_CLIENT_ID,
|
|
'client_secret': STRAVA_CLIENT_SECRET,
|
|
'callback_url': webhooklink,
|
|
'verify_token': webhookverification,
|
|
}
|
|
headers = {'user-agent': 'sanderroosendaal',
|
|
'Accept': 'application/json',
|
|
'Content-Type': oauth_data['content_type']}
|
|
|
|
response = requests.post(url,data=post_data)
|
|
|
|
return response.status_code
|
|
|
|
def strava_list_push(): # pragma: no cover
|
|
url = "https://www.strava.com/api/v3/push_subscriptions"
|
|
params = {
|
|
'client_id': STRAVA_CLIENT_ID,
|
|
'client_secret': STRAVA_CLIENT_SECRET,
|
|
|
|
}
|
|
response = requests.get(url,params=params)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return [w['id'] for w in data]
|
|
return []
|
|
|
|
def strava_push_delete(id): # pragma: no cover
|
|
url = "https://www.strava.com/api/v3/push_subscriptions/{id}".format(id=id)
|
|
params = {
|
|
'client_id': STRAVA_CLIENT_ID,
|
|
'client_secret': STRAVA_CLIENT_SECRET,
|
|
}
|
|
response = requests.delete(url,json=params)
|
|
return response.status_code
|
|
|
|
|
|
def set_strava_athlete_id(user):
|
|
r = Rower.objects.get(user=user)
|
|
if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover
|
|
s = "Token doesn't exist. Need to authorize"
|
|
return custom_exception_handler(401,s)
|
|
elif (r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599)>r.stravatokenexpirydate):
|
|
token = imports_open(user,oauth_data)
|
|
|
|
authorizationstring = str('Bearer ' + r.stravatoken)
|
|
headers = {'Authorization': authorizationstring,
|
|
'user-agent': 'sanderroosendaal',
|
|
'Content-Type': 'application/json'}
|
|
url = "https://www.strava.com/api/v3/athlete"
|
|
|
|
response = requests.get(url,headers=headers,params={})
|
|
|
|
if response.status_code == 200: # pragma: no cover
|
|
r.strava_owner_id = response.json()['id']
|
|
r.save()
|
|
return response.json()['id']
|
|
|
|
return 0
|
|
|
|
|
|
# Get list of workouts available on Strava
|
|
def get_strava_workout_list(user,limit_n=0):
|
|
r = Rower.objects.get(user=user)
|
|
|
|
if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover
|
|
s = "Token doesn't exist. Need to authorize"
|
|
return custom_exception_handler(401,s)
|
|
elif (r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599)>r.stravatokenexpirydate): # pragma: no cover
|
|
s = "Token expired. Needs to refresh."
|
|
return custom_exception_handler(401,s)
|
|
else:
|
|
# ready to fetch. Hurray
|
|
authorizationstring = str('Bearer ' + r.stravatoken)
|
|
headers = {'Authorization': authorizationstring,
|
|
'user-agent': 'sanderroosendaal',
|
|
'Content-Type': 'application/json'}
|
|
|
|
url = "https://www.strava.com/api/v3/athlete/activities"
|
|
|
|
if limit_n==0:
|
|
params = {}
|
|
else: # pragma: no cover
|
|
params = {'per_page':limit_n}
|
|
|
|
s = requests.get(url,headers=headers,params=params)
|
|
|
|
|
|
return s
|
|
|
|
|
|
from rowers.utils import get_strava_stream
|
|
|
|
def async_get_workout(user,stravaid):
|
|
try:
|
|
token = strava_open(user)
|
|
except NoTokenError: # pragma: no cover
|
|
return 0
|
|
|
|
csvfilename = 'media/{code}_{stravaid}.csv'.format(code=uuid4().hex[:16],stravaid=stravaid)
|
|
job = myqueue(queue,
|
|
fetch_strava_workout,
|
|
user.rower.stravatoken,
|
|
oauth_data,
|
|
stravaid,
|
|
csvfilename,
|
|
user.id,
|
|
)
|
|
return job
|
|
|
|
# Get a Strava workout summary data and stroke data by ID
|
|
def get_workout(user,stravaid,do_async=True):
|
|
return async_get_workout(user,stravaid)
|
|
|
|
|
|
# Generate Workout data for Strava (a TCX file)
|
|
def createstravaworkoutdata(w,dozip=True):
|
|
filename = w.csvfilename
|
|
try:
|
|
row = rowingdata(csvfile=filename)
|
|
except IOError: # pragma: no cover
|
|
data = dataprep.read_df_sql(w.id)
|
|
try:
|
|
datalength = len(data)
|
|
except AttributeError:
|
|
datalength = 0
|
|
|
|
if datalength != 0:
|
|
data.rename(columns = columndict,inplace=True)
|
|
res = data.to_csv(w.csvfilename+'.gz',
|
|
index_label='index',
|
|
compression='gzip')
|
|
try:
|
|
row = rowingdata(csvfile=filename)
|
|
except IOError:
|
|
return '','Error - could not find rowing data'
|
|
else:
|
|
return '','Error - could not find rowing data'
|
|
|
|
tcxfilename = filename[:-4]+'.tcx'
|
|
try:
|
|
newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
|
|
except TypeError:
|
|
newnotes = 'from '+w.workoutsource+' via rowsandall.com'
|
|
|
|
row.exporttotcx(tcxfilename,notes=newnotes)
|
|
if dozip:
|
|
gzfilename = tcxfilename+'.gz'
|
|
with open(tcxfilename,'rb') as inF:
|
|
s = inF.read()
|
|
with gzip.GzipFile(gzfilename,'wb') as outF:
|
|
outF.write(s)
|
|
|
|
try:
|
|
os.remove(tcxfilename)
|
|
except WindowError: # pragma: no cover
|
|
pass
|
|
|
|
return gzfilename,""
|
|
|
|
|
|
return tcxfilename,""
|
|
|
|
|
|
# Upload the TCX file to Strava and set the workout activity type
|
|
# to rowing on Strava
|
|
def handle_stravaexport(f2,workoutname,stravatoken,description='',
|
|
activity_type='Rowing',quick=False,asynchron=False):
|
|
# w = Workout.objects.get(id=workoutid)
|
|
client = stravalib.Client(access_token=stravatoken)
|
|
|
|
act = client.upload_activity(f2,'tcx.gz',name=workoutname)
|
|
|
|
try:
|
|
if quick: # pragma: no cover
|
|
res = act.wait(poll_interval=2.0, timeout=10)
|
|
message = 'Workout successfully synchronized to Strava'
|
|
else:
|
|
res = act.wait(poll_interval=5.0,timeout=30)
|
|
message = 'Workout successfully synchronized to Strava'
|
|
except: # pragma: no cover
|
|
res = 0
|
|
message = 'Strava upload timed out'
|
|
|
|
|
|
# description doesn't work yet. Have to wait for stravalib to update
|
|
if res:
|
|
try:
|
|
act = client.update_activity(res.id,activity_type=activity_type,description=description,device_name='Rowsandall.com')
|
|
except TypeError: # pragma: no cover
|
|
act = client.update_activity(res.id,activity_type=activity_type,description=description)
|
|
else: # pragma: no cover
|
|
message = 'Strava activity update timed out.'
|
|
return (0,message)
|
|
|
|
return (res.id,message)
|
|
|
|
|
|
def workout_strava_upload(user,w, quick=False,asynchron=True):
|
|
try:
|
|
thetoken = strava_open(user)
|
|
except NoTokenError: # pragma: no cover
|
|
return "Please connect to Strava first",0
|
|
|
|
message = "Uploading to Strava"
|
|
stravaid=-1
|
|
r = Rower.objects.get(user=user)
|
|
res = -1
|
|
if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover
|
|
s = "Token doesn't exist. Need to authorize"
|
|
raise NoTokenError("Your hovercraft is full of eels")
|
|
|
|
if (is_workout_user(user,w)):
|
|
if asynchron:
|
|
tcxfile, tcxmesg = createstravaworkoutdata(w)
|
|
if not tcxfile: # pragma: no cover
|
|
return "Failed to create workout data",0
|
|
activity_type = r.stravaexportas
|
|
if r.stravaexportas == 'match':
|
|
try:
|
|
activity_type = mytypes.stravamapping[w.workouttype]
|
|
except KeyError: # pragma: no cover
|
|
activity_type = 'Rowing'
|
|
job = myqueue(queue,
|
|
handle_strava_sync,
|
|
r.stravatoken,
|
|
w.id,
|
|
tcxfile,w.name,activity_type,
|
|
w.notes
|
|
)
|
|
dologging('strava_export_log.log','Exporting as {t} from {w}'.format(t=activity_type,w=w.workouttype))
|
|
return "Asynchronous sync",-1
|
|
try:
|
|
tcxfile,tcxmesg = createstravaworkoutdata(w)
|
|
if tcxfile:
|
|
activity_type = r.stravaexportas
|
|
if r.stravaexportas == 'match':
|
|
try:
|
|
activity_type = mytypes.stravamapping[w.workouttype]
|
|
except KeyError: # pragma: no cover
|
|
activity_type = 'Rowing'
|
|
|
|
with open(tcxfile,'rb') as f:
|
|
try:
|
|
description = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
|
|
except TypeError:
|
|
description = ' via rowsandall.com'
|
|
res,mes = handle_stravaexport(
|
|
f,w.name,
|
|
r.stravatoken,
|
|
description=description,
|
|
activity_type=activity_type,quick=quick,asynchron=asynchron)
|
|
if res==0: # pragma: no cover
|
|
message = mes
|
|
w.uploadedtostrava = -1
|
|
stravaid = -1
|
|
w.save()
|
|
try:
|
|
os.remove(tcxfile)
|
|
except WindowsError:
|
|
pass
|
|
return message,stravaid
|
|
|
|
w.uploadedtostrava = res
|
|
w.save()
|
|
try:
|
|
os.remove(tcxfile)
|
|
except WindowsError: # pragma: no cover
|
|
pass
|
|
message = mes
|
|
stravaid = res
|
|
return message,stravaid
|
|
else: # pragma: no cover
|
|
message = "Strava TCX data error "+tcxmesg
|
|
w.uploadedtostrava = -1
|
|
stravaid = -1
|
|
w.save()
|
|
return message, stravaid
|
|
|
|
except ActivityUploadFailed as e: # pragma: no cover
|
|
message = "Strava Upload error: %s" % e
|
|
w.uploadedtostrava = -1
|
|
stravaid = -1
|
|
w.save()
|
|
os.remove(tcxfile)
|
|
return message,stravaid
|
|
return message,stravaid # pragma: no cover
|