online_sync.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/usr/bin/env python3
  2. #
  3. # Adapted from https://github.com/jlemon/zlogger/blob/master/get_riders.py
  4. #
  5. # The MIT License (MIT)
  6. #
  7. # Copyright (c) 2016 Jonathan Lemon
  8. #
  9. # Permission is hereby granted, free of charge, to any person obtaining a copy
  10. # of this software and associated documentation files (the "Software"), to deal
  11. # in the Software without restriction, including without limitation the rights
  12. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. # copies of the Software, and to permit persons to whom the Software is
  14. # furnished to do so, subject to the following conditions:
  15. #
  16. # The above copyright notice and this permission notice shall be included in all
  17. # copies or substantial portions of the Software.
  18. #
  19. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. # FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
  22. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  25. # SOFTWARE.
  26. import json
  27. import requests
  28. import profile_pb2
  29. def post_credentials(session, username, password):
  30. # Credentials POSTing and tokens retrieval
  31. # POST https://secure.zwift.com/auth/realms/zwift/tokens/access/codes
  32. try:
  33. response = session.post(
  34. url="https://secure.zwift.com/auth/realms/zwift/tokens/access/codes",
  35. headers={
  36. "Accept": "*/*",
  37. "Accept-Encoding": "gzip, deflate",
  38. "Connection": "keep-alive",
  39. "Content-Type": "application/x-www-form-urlencoded",
  40. "Host": "secure.zwift.com",
  41. "User-Agent": "Zwift/1.5 (iPhone; iOS 9.0.2; Scale/2.00)",
  42. "Accept-Language": "en-US;q=1",
  43. },
  44. data={
  45. "client_id": "Zwift_Mobile_Link",
  46. "username": username,
  47. "password": password,
  48. "grant_type": "password",
  49. },
  50. allow_redirects=False,
  51. )
  52. json_dict = json.loads(response.content)
  53. return (json_dict["access_token"], json_dict["refresh_token"], json_dict["expires_in"])
  54. except requests.exceptions.RequestException as e:
  55. print('HTTP Request failed: %s' % e)
  56. except KeyError as e:
  57. print('Invalid uname and/or password')
  58. def query(session, access_token, route):
  59. try:
  60. response = session.get(
  61. url="https://us-or-rly101.zwift.com/%s" % route,
  62. headers={
  63. "Accept-Encoding": "gzip, deflate",
  64. "Accept": "application/x-protobuf-lite",
  65. "Connection": "keep-alive",
  66. "Host": "us-or-rly101.zwift.com",
  67. "User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0",
  68. "Authorization": "Bearer %s" % access_token,
  69. "Accept-Language": "en-us",
  70. },
  71. )
  72. return response.content
  73. except requests.exceptions.RequestException as e:
  74. print('HTTP Request failed: %s' % e)
  75. def api_login(session, access_token, login_request):
  76. try:
  77. response = session.post(
  78. url="https://us-or-rly101.zwift.com/api/users/login",
  79. headers={
  80. "Content-Type": "application/x-protobuf-lite",
  81. "Accept": "application/x-protobuf-lite",
  82. "Connection": "keep-alive",
  83. "Host": "us-or-rly101.zwift.com",
  84. "User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0",
  85. "Authorization": "Bearer %s" % access_token,
  86. "Accept-Language": "en-us",
  87. },
  88. data=login_request.SerializeToString(),
  89. )
  90. return response.content
  91. except requests.exceptions.RequestException as e:
  92. print('HTTP Request failed: %s' % e)
  93. def logout(session, refresh_token):
  94. # Logout
  95. # POST https://secure.zwift.com/auth/realms/zwift/tokens/logout
  96. try:
  97. response = session.post(
  98. url="https://secure.zwift.com/auth/realms/zwift/tokens/logout",
  99. headers={
  100. "Accept": "*/*",
  101. "Accept-Encoding": "gzip, deflate",
  102. "Connection": "keep-alive",
  103. "Content-Type": "application/x-www-form-urlencoded",
  104. "Host": "secure.zwift.com",
  105. "User-Agent": "Zwift/1.5 (iPhone; iOS 9.0.2; Scale/2.00)",
  106. "Accept-Language": "en-US;q=1",
  107. },
  108. data={
  109. "client_id": "Zwift_Mobile_Link",
  110. "refresh_token": refresh_token,
  111. },
  112. )
  113. except requests.exceptions.RequestException as e:
  114. print('HTTP Request failed: %s' % e)
  115. def login(session, user, password):
  116. access_token, refresh_token, expired_in = post_credentials(session, user, password)
  117. return access_token, refresh_token
  118. def create_activity(session, access_token, activity):
  119. try:
  120. response = session.post(
  121. url="https://us-or-rly101.zwift.com/api/profiles/%s/activities" % activity.player_id,
  122. headers={
  123. "Content-Type": "application/x-protobuf-lite",
  124. "Accept": "application/json",
  125. "Connection": "keep-alive",
  126. "Host": "us-or-rly101.zwift.com",
  127. "User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0",
  128. "Authorization": "Bearer %s" % access_token,
  129. "Accept-Language": "en-us",
  130. },
  131. data=activity.SerializeToString(),
  132. )
  133. json_dict = json.loads(response.content)
  134. return json_dict["id"]
  135. except requests.exceptions.RequestException as e:
  136. print('HTTP Request failed: %s' % e)
  137. def upload_activity(session, access_token, activity):
  138. try:
  139. response = session.put(
  140. url="https://us-or-rly101.zwift.com/api/profiles/%s/activities/%s" % (activity.player_id, activity.id),
  141. headers={
  142. "Content-Type": "application/x-protobuf-lite",
  143. "Accept": "application/json",
  144. "Connection": "keep-alive",
  145. "Host": "us-or-rly101.zwift.com",
  146. "User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0",
  147. "Authorization": "Bearer %s" % access_token,
  148. "Accept-Language": "en-us",
  149. },
  150. data=activity.SerializeToString(),
  151. params={"upload-to-strava": "true"}
  152. )
  153. return response.status_code
  154. except requests.exceptions.RequestException as e:
  155. print('HTTP Request failed: %s' % e)
  156. def get_player_id(session, access_token):
  157. try:
  158. response = session.get(
  159. url="https://us-or-rly101.zwift.com/api/profiles/me",
  160. headers={
  161. "Accept-Encoding": "gzip, deflate",
  162. "Accept": "application/x-protobuf-lite",
  163. "Connection": "keep-alive",
  164. "Host": "us-or-rly101.zwift.com",
  165. "User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0",
  166. "Authorization": "Bearer %s" % access_token,
  167. "Accept-Language": "en-us",
  168. },
  169. )
  170. profile = profile_pb2.PlayerProfile()
  171. profile.ParseFromString(response.content)
  172. return profile.id
  173. except requests.exceptions.RequestException as e:
  174. print('HTTP Request failed: %s' % e)