From 6d6741b90aff7acc2e41ee7d2c7706841a1df4ab Mon Sep 17 00:00:00 2001 From: Neil Boyd Date: Sun, 3 Jan 2021 17:24:11 +0100 Subject: [PATCH 01/60] copy everything from original PR --- requirements.txt | 1 + tapiriik/services/MapMyFitness/__init__.py | 1 + .../services/MapMyFitness/mapmyfitness.py | 153 ++++++++++++++++++ tapiriik/services/__init__.py | 2 + tapiriik/services/service.py | 4 +- tapiriik/services/service_base.py | 1 + tapiriik/web/static/css/style.css | 5 + .../web/static/img/services/mapmyfitness.png | Bin 0 -> 2266 bytes .../static/img/services/mapmyfitness_l.png | Bin 0 -> 2977 bytes tapiriik/web/static/js/tapiriik.js | 2 + 10 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 tapiriik/services/MapMyFitness/__init__.py create mode 100644 tapiriik/services/MapMyFitness/mapmyfitness.py create mode 100644 tapiriik/web/static/img/services/mapmyfitness.png create mode 100644 tapiriik/web/static/img/services/mapmyfitness_l.png diff --git a/requirements.txt b/requirements.txt index bb13abcf3..0758da3da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ requests +requests-oauthlib dnspython pymongo==3.10.1 pytz diff --git a/tapiriik/services/MapMyFitness/__init__.py b/tapiriik/services/MapMyFitness/__init__.py new file mode 100644 index 000000000..7955bbcf7 --- /dev/null +++ b/tapiriik/services/MapMyFitness/__init__.py @@ -0,0 +1 @@ +from .mapmyfitness import * diff --git a/tapiriik/services/MapMyFitness/mapmyfitness.py b/tapiriik/services/MapMyFitness/mapmyfitness.py new file mode 100644 index 000000000..9fefe6944 --- /dev/null +++ b/tapiriik/services/MapMyFitness/mapmyfitness.py @@ -0,0 +1,153 @@ +from tapiriik.services.service_authentication import ServiceAuthenticationType +from tapiriik.services.api import APIException, APIAuthorizationException +from tapiriik.services.interchange import UploadedActivity, ActivityType, WaypointType, Waypoint, Location +from tapiriik.settings import WEB_ROOT, MAPMYFITNESS_CLIENT_KEY, MAPMYFITNESS_CLIENT_SECRET + +from datetime import datetime, timedelta +import requests +from django.core.urlresolvers import reverse +from requests_oauthlib import OAuth1 + + +class MapMyFitnessService(): + ID = "mapmyfitness" + DisplayName = "MapMyFitness" + AuthenticationType = ServiceAuthenticationType.OAuthSigned + UserAuthorizationURL = None + OutstandingOAuthRequestTokens = {} + + _activityMappings = {16: ActivityType.Running, + 11: ActivityType.Cycling, + 41: ActivityType.MountainBiking, + 9: ActivityType.Walking, + 24: ActivityType.Hiking, + 398: ActivityType.DownhillSkiing, + 397: ActivityType.CrossCountrySkiing, # actually "backcountry" :S + 107: ActivityType.Snowboarding, + 86: ActivityType.Skating, # ice skating + 15: ActivityType.Swimming, + 57: ActivityType.Rowing, # canoe/rowing + 211: ActivityType.Elliptical, + 21: ActivityType.Other} + SupportedActivities = list(_activityMappings.values()) + + def WebInit(self): + pass + + def GenerateUserAuthorizationURL(self): + oauth = OAuth1(MAPMYFITNESS_CLIENT_KEY, client_secret=MAPMYFITNESS_CLIENT_SECRET) + response = requests.post("http://api.mapmyfitness.com/3.1/oauth/request_token", auth=oauth) + from urllib.parse import parse_qs, urlencode + credentials = parse_qs(response.text) + token = credentials["oauth_token"][0] + self.OutstandingOAuthRequestTokens[token] = credentials["oauth_token_secret"][0] + reqObj = {"oauth_token": token, "oauth_callback": WEB_ROOT + reverse("oauth_return", kwargs={"service": "mapmyfitness"})} + return "http://api.mapmyfitness.com/3.1/oauth/authorize?" + urlencode(reqObj) + + def _getOauthClient(self, svcRec): + return OAuth1(MAPMYFITNESS_CLIENT_KEY, + client_secret=MAPMYFITNESS_CLIENT_SECRET, + resource_owner_key=svcRec["Authorization"]["Key"], + resource_owner_secret=svcRec["Authorization"]["Secret"]) + + def _getUserId(self, svcRec): + oauth = self._getOauthClient(svcRec) + response = requests.get("http://api.mapmyfitness.com/3.1/users/get_user", auth=oauth) + responseData = response.json() + return responseData["result"]["output"]["user"]["user_id"] + + def RetrieveAuthorizationToken(self, req): + from tapiriik.services import Service + + token = req.GET.get("oauth_token") + + oauth = OAuth1(MAPMYFITNESS_CLIENT_KEY, + client_secret=MAPMYFITNESS_CLIENT_SECRET, + resource_owner_key=token, + resource_owner_secret=self.OutstandingOAuthRequestTokens[token]) + + response = requests.post("http://api.mapmyfitness.com/3.1/oauth/access_token", auth=oauth) + if response.status_code != 200: + raise APIAuthorizationException("Invalid code", None) + + del self.OutstandingOAuthRequestTokens[token] + + from urllib.parse import parse_qs + + responseData = parse_qs(response.text) + + token = responseData["oauth_token"][0] + secret = responseData["oauth_token_secret"][0] + + # hacky, but also totally their fault for not giving the user id in the token req + existingRecord = Service.GetServiceRecordWithAuthDetails(self, {"Key": token}) + if existingRecord is None: + uid = self._getUserId({"Authorization": {"Key": token, "Secret": secret}}) # meh + else: + uid = existingRecord["ExternalID"] + return (uid, {"Key": token, "Secret": secret}) + + def RevokeAuthorization(self, serviceRecord): + oauth = self._getOauthClient(serviceRecord) + resp = requests.post("http://api.mapmyfitness.com/3.1/oauth/revoke", auth=oauth) + if resp.status_code != 200: + raise APIException("Unable to deauthorize MMF auth token, status " + str(resp.status_code) + " resp " + resp.text, serviceRecord) + + def _getActivityTypeHierarchy(self): + if hasattr(self, "_activityTypes"): + return self._activityTypes + response = requests.get("http://api.mapmyfitness.com/3.1/workouts/get_activity_types") + data = response.json() + self._activityTypes = {} + for actType in data["result"]["output"]["activity_types"]: + self._activityTypes[int(actType["activity_type_id"])] = actType + return self._activityTypes + + def _resolveActivityType(self, actType): + self._getActivityTypeHierarchy() + while actType not in self._activityMappings or self._activityTypes[actType]["parent_activity_type_id"] is not None: + actType = int(self._activityTypes[actType]["parent_activity_type_id"]) + if actType in self._activityMappings: + return self._activityMappings[actType] + else: + return ActivityType.Other + + def DownloadActivityList(self, serviceRecord, exhaustive=False): + oauth = self._getOauthClient(serviceRecord) + + allItems = [] + + offset = 0 + + while True: + response = requests.get("http://api.mapmyfitness.com/3.1/workouts/get_workouts?limit=25&start_record=" + str(offset), auth=oauth) + if response.status_code != 200: + if response.status_code == 401 or response.status_code == 403: + raise APIAuthorizationException("No authorization to retrieve activity list", serviceRecord) + raise APIException("Unable to retrieve activity list " + str(response), serviceRecord) + data = response.json() + print(data) + allItems += data["result"]["output"]["workouts"] + if not exhaustive or int(data["result"]["output"]["count"]) < 25: + break + + activities = [] + for act in allItems: + activity = UploadedActivity() + activity.StartTime = datetime.strptime(act["workout_date"] + " " + act["workout_start_time"], "%Y-%m-%d %H:%M:%S") + activity.EndTime = activity.StartTime + timedelta(0, round(float(act["time_taken"]))) + activity.Distance = act["distance"] + + activity.Type = self._resolveActivityType(int(act["activity_type_id"])) + activity.CalculateUID() + + activity.UploadedTo = [{"Connection": serviceRecord, "ActivityID": act["workout_id"]}] + activities.append(activity) + return activities + + def DownloadActivity(self, serviceRecord, activity): + activityID = [x["ActivityID"] for x in activity.UploadedTo if x["Connection"] == serviceRecord][0] + print (activityID)# route id 175411456 key 2025466620 + oauth = self._getOauthClient(serviceRecord) + response = requests.get("http://api.mapmyfitness.com/3.1/routes/get_routes", auth=oauth) + print (response.text) diff --git a/tapiriik/services/__init__.py b/tapiriik/services/__init__.py index c4bd65421..170df32b3 100644 --- a/tapiriik/services/__init__.py +++ b/tapiriik/services/__init__.py @@ -38,6 +38,8 @@ Singletracker = SingletrackerService() from tapiriik.services.Aerobia import AerobiaService Aerobia = AerobiaService() +from tapiriik.services.MapMyFitness import MapMyFitnessService +MapMyFitness = MapMyFitnessService() PRIVATE_SERVICES = [] try: diff --git a/tapiriik/services/service.py b/tapiriik/services/service.py index 93af9e8d2..b92d6de79 100644 --- a/tapiriik/services/service.py +++ b/tapiriik/services/service.py @@ -47,6 +47,7 @@ def List(): Setio, Singletracker, Aerobia, + MapMyFitness, private_svc_map.get("runsense") ) return tuple(x for x in svc_list if x is not None) @@ -74,7 +75,8 @@ def PreferredDownloadPriorityList(): Pulsstory, Setio, Singletracker, - Aerobia + Aerobia, + MapMyFitness ] + PRIVATE_SERVICES def WebInit(): diff --git a/tapiriik/services/service_base.py b/tapiriik/services/service_base.py index 13b7280d8..9074f4c3d 100644 --- a/tapiriik/services/service_base.py +++ b/tapiriik/services/service_base.py @@ -3,6 +3,7 @@ class ServiceAuthenticationType: OAuth = "oauth" + OAuthSigned = "oauth-signed" UsernamePassword = "direct" class InvalidServiceOperationException(Exception): diff --git a/tapiriik/web/static/css/style.css b/tapiriik/web/static/css/style.css index 324d9a5f0..184181b40 100644 --- a/tapiriik/web/static/css/style.css +++ b/tapiriik/web/static/css/style.css @@ -121,6 +121,11 @@ body { height:475px; } +.popover iframe#mapmyfitness { + width:400px; + height:680px; +} + .logo { width:100%; position:absolute; diff --git a/tapiriik/web/static/img/services/mapmyfitness.png b/tapiriik/web/static/img/services/mapmyfitness.png new file mode 100644 index 0000000000000000000000000000000000000000..575f2892ead244e2fc24e423380ffe809b8ce3b4 GIT binary patch literal 2266 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 literal 0 HcmV?d00001 diff --git a/tapiriik/web/static/js/tapiriik.js b/tapiriik/web/static/js/tapiriik.js index df2c3e35f..a6889a590 100644 --- a/tapiriik/web/static/js/tapiriik.js +++ b/tapiriik/web/static/js/tapiriik.js @@ -204,6 +204,8 @@ tapiriik.OpenAuthDialog = function(svcId){ } else { contents = $("