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 .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 rowingdata import rowingdata
from rowers.rower_rules import user_is_not_basic, user_is_coachee
from rowers import mytypes from rowers import mytypes
@@ -49,6 +50,8 @@ headers = {
intervals_authorize_url = 'https://intervals.icu/oauth/authorize?' intervals_authorize_url = 'https://intervals.icu/oauth/authorize?'
intervals_token_url = 'https://intervals.icu/api/oauth/token' intervals_token_url = 'https://intervals.icu/api/oauth/token'
webhookverification = 'JA9Vt6RNH10'
class IntervalsIntegration(SyncIntegration): class IntervalsIntegration(SyncIntegration):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(IntervalsIntegration, self).__init__(*args, **kwargs) super(IntervalsIntegration, self).__init__(*args, **kwargs)
@@ -335,6 +338,28 @@ class IntervalsIntegration(SyncIntegration):
data = response.json() data = response.json()
return data 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): def get_plannedsession(self, id, *args, **kwargs):
_ = self.open() _ = self.open()
@@ -396,6 +421,7 @@ class IntervalsIntegration(SyncIntegration):
"name": ps.name, "name": ps.name,
"description": stepstext, "description": stepstext,
"indoor": ps.sessionsport in mytypes.ergtypes, "indoor": ps.sessionsport in mytypes.ergtypes,
'external_id': ps.id,
} }
if ps.sessiontype == 'cycletarget': if ps.sessiontype == 'cycletarget':
@@ -442,3 +468,119 @@ class IntervalsIntegration(SyncIntegration):
ps.save() ps.save()
return 1 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_export = models.BooleanField(default=False)
intervals_auto_import = 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_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') intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
sporttrackstoken = models.CharField( sporttrackstoken = models.CharField(
default='', max_length=200, blank=True, null=True) default='', max_length=200, blank=True, null=True)
@@ -4676,6 +4677,7 @@ class RowerExportForm(ModelForm):
'intervals_auto_export', 'intervals_auto_export',
'intervals_delete_plannedsession', 'intervals_delete_plannedsession',
'intervals_resample_to_1s', 'intervals_resample_to_1s',
'intervals_auto_delete',
'imports_are_private' 'imports_are_private'
] ]
@@ -4703,6 +4705,7 @@ class RowerExportFormIntervals(ModelForm):
'intervals_auto_import', 'intervals_auto_import',
'intervals_auto_export', 'intervals_auto_export',
'intervals_resample_to_1s', 'intervals_resample_to_1s',
'intervals_auto_delete',
'intervals_delete_plannedsession', 'intervals_delete_plannedsession',
] ]
Binary file not shown.
+1
View File
@@ -633,6 +633,7 @@ urlpatterns = [
name='workout_rojaboimport_view'), name='workout_rojaboimport_view'),
re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view, re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view,
name='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/$', re_path(r'^workout/(?P<source>\w+.*)import/$',
views.workout_import_view, name='workout_import_view'), views.workout_import_view, name='workout_import_view'),
re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$', 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 from rowers.models import PlannedSession
import rowers.integrations.strava as strava import rowers.integrations.strava as strava
import rowers.integrations.intervals as intervals
from rowers.integrations import importsources from rowers.integrations import importsources
from rowers.integrations import IntervalsIntegration
from rowers.utils import NoTokenError from rowers.utils import NoTokenError
import numpy 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 # for Strava webhook request validation
@csrf_exempt @csrf_exempt
def strava_webhook_view(request): def strava_webhook_view(request):
if request.method == 'GET': if request.method == 'GET':