Private
Public Access
1
0

Merge branch 'feature/intervals_webhook' into develop

This commit is contained in:
2024-12-26 10:37:43 +01:00
5 changed files with 204 additions and 3 deletions
+143 -1
View File
@@ -1,6 +1,7 @@
from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids
from rowers.models import Rower, User, Workout, TombStone
from rowers.models import Rower, User, Workout, TombStone, PlannedSession
from rowingdata import rowingdata
from rowers.rower_rules import user_is_not_basic, user_is_coachee
from rowers import mytypes
@@ -49,6 +50,8 @@ headers = {
intervals_authorize_url = 'https://intervals.icu/oauth/authorize?'
intervals_token_url = 'https://intervals.icu/api/oauth/token'
webhookverification = 'JA9Vt6RNH10'
class IntervalsIntegration(SyncIntegration):
def __init__(self, *args, **kwargs):
super(IntervalsIntegration, self).__init__(*args, **kwargs)
@@ -336,6 +339,28 @@ class IntervalsIntegration(SyncIntegration):
return data
def update_plannedsession(self, ps, data, *args, **kwargs):
_ = self.open()
r = self.rower
if data['category'] == 'WORKOUT':
url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(ps.intervals_icu_id) + '/downloadfit'
headers = {
'Authorization': 'Bearer ' + r.intervals_token,
}
response = requests.get(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
else:
filename = 'planned_' + str(ps.intervals_icu_id) + '.fit'
filename2 = 'media/planned_' + str(ps.intervals_icu_id) + '.fit'
with open(filename2, 'wb') as f:
f.write(response.content)
data['fitfile'] = filename
return data
def get_plannedsession(self, id, *args, **kwargs):
_ = self.open()
r = self.rower
@@ -396,6 +421,7 @@ class IntervalsIntegration(SyncIntegration):
"name": ps.name,
"description": stepstext,
"indoor": ps.sessionsport in mytypes.ergtypes,
'external_id': ps.id,
}
if ps.sessiontype == 'cycletarget':
@@ -442,3 +468,119 @@ class IntervalsIntegration(SyncIntegration):
ps.save()
return 1
def update_calendar(self, r, event, *args, **kwargs):
try:
records = event["events"]
except KeyError:
records = []
for record in records:
id = record['id']
data = {}
try:
pss = PlannedSession.objects.filter(intervals_icu_id=id)
if pss.count() > 0:
ps = pss[0]
data = self.update_plannedsession(ps, record)
else:
data = self.get_plannedsession(id)
ps = PlannedSession(
manager=r.user,
intervals_icu_id=id,
)
ps.save()
ps.rower.add(r)
except PlannedSession.DoesNotExist:
continue
# got data
if data:
ps.name = data['name']
ps.comment = data['description']
ps.startdate = arrow.get(data['start_date_local']).datetime
ps.enddate = arrow.get(data['end_date_local']).datetime
ps.preferreddate = arrow.get(data['start_date_local']).datetime
ps.sessionsport = mytypes.intervalsmappinginv[data['type']]
ps.sessiontype = 'session'
ps.save()
try:
timetarget = data['time_target']
except KeyError:
timetarget = None
if timetarget is None:
try:
timetarget = data['moving_time']
except KeyError:
timetarget = None
if timetarget is None:
timetarget = 3600
timetarget = int(timetarget)/60.
ps.sessionvalue = timetarget
ps.save()
if data['category'].lower() == 'workout':
ps.fitfile = data['fitfile']
ps.save()
ps.update_steps()
if data['category'].lower() == 'target':
ps.sessiontype = 'cycletarget'
ps.sessionvalue = int(data['time_target'])/60.
ps.enddate = ps.startdate + datetime.timedelta(days=6)
ps.save()
try:
deleted_records = event["deleted_events"]
except KeyError:
deleted_records = []
for record in deleted_records:
id = record['id']
try:
pss = PlannedSession.objects.filter(intervals_icu_id=id)
if r.intervals_delete_plannedsession and pss.count() > 0:
for ps in pss:
ps.delete()
except PlannedSession.DoesNotExist:
continue
return 1
def import_activities(self, event, *args, **kwargs):
if not self.rower.intervals_auto_import:
return 0
if user_is_not_basic(self.rower.user) or user_is_coachee(self.rower.user):
try:
record = event["activity"]
except KeyError:
records = []
try:
id = record['id']
result = self.get_workout(id)
except KeyError:
pass
return 1
return 0
def delete_activities(self, event, *args, **kwargs):
if not self.rower.intervals_auto_delete:
return 0
try:
record = event["activity"]
except KeyError:
records = []
try:
id = record['id']
try:
w = Workout.objects.get(uploadedtointervals=id)
if w.user == self.rower:
w.delete()
except Workout.DoesNotExist:
pass
except KeyError:
pass
return 1
+3
View File
@@ -1176,6 +1176,7 @@ class Rower(models.Model):
intervals_auto_export = models.BooleanField(default=False)
intervals_auto_import = models.BooleanField(default=False)
intervals_delete_plannedsession = models.BooleanField(default=False, verbose_name="Deleting planned session deletes it on intervals.icu")
intervals_auto_delete = models.BooleanField(default=True, verbose_name="Deleting an activity on intervals.icu deletes it on both")
intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
sporttrackstoken = models.CharField(
default='', max_length=200, blank=True, null=True)
@@ -4676,6 +4677,7 @@ class RowerExportForm(ModelForm):
'intervals_auto_export',
'intervals_delete_plannedsession',
'intervals_resample_to_1s',
'intervals_auto_delete',
'imports_are_private'
]
@@ -4703,6 +4705,7 @@ class RowerExportFormIntervals(ModelForm):
'intervals_auto_import',
'intervals_auto_export',
'intervals_resample_to_1s',
'intervals_auto_delete',
'intervals_delete_plannedsession',
]
Binary file not shown.
+1
View File
@@ -633,6 +633,7 @@ urlpatterns = [
name='workout_rojaboimport_view'),
re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view,
name='plannedsession_intervalsimport_view'),
re_path(r'^session/intervals/webhook/$', views.intervals_webhook_view, name='intervals_webhook_view'),
re_path(r'^workout/(?P<source>\w+.*)import/$',
views.workout_import_view, name='workout_import_view'),
re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$',
+57 -2
View File
@@ -10,7 +10,9 @@ from rowers.utils import NoTokenError
from rowers.models import PlannedSession
import rowers.integrations.strava as strava
import rowers.integrations.intervals as intervals
from rowers.integrations import importsources
from rowers.integrations import IntervalsIntegration
from rowers.utils import NoTokenError
import numpy
@@ -913,9 +915,62 @@ def workout_rojaboimport_view(request, message="", userid=0): # pragma: no cover
@csrf_exempt
def intervals_webhook_view(request):
if request.method == 'GET':
verificationtoken = request.GET.get('secret')
if verificationtoken != intervals.webhookverification:
return HttpResponse(status=403)
dologging("intervals_webhooks.log","GET request")
dologging("intervals_webhooks.log",request.body)
else:
data = json.loads(request.body)
try:
verificationtoken = data['secret']
except KeyError:
return HttpResponse(status=403)
if verificationtoken != intervals.webhookverification:
return HttpResponse(status=403)
try:
events = data['events']
except KeyError:
# return invalid request if no events
return HttpResponse(status=200)
webhook_type = None
for event in events:
try:
athlete_id = event['athlete_id']
webhook_type = event['type']
r = Rower.objects.get(intervals_owner_id=athlete_id)
except Rower.DoesNotExist:
return HttpResponse(status=200)
except MultipleObjectsReturned:
rs = Rower.objects.filter(intervals_owner_id=athlete_id)
r = rs[0]
except KeyError:
return HttpResponse(status=200)
integration = IntervalsIntegration(r.user)
if webhook_type.lower() == 'calendar_updated':
integration.update_calendar(r, event)
if webhook_type.lower() == 'activity_uploaded':
integration.import_activities(event)
if webhook_type.lower() == 'activity_deleted':
integration.delete_activities(event)
return HttpResponse(status=200)
# for Strava webhook request validation
@csrf_exempt
def strava_webhook_view(request):
if request.method == 'GET':