Using the Microsoft Graph API to retrieve Calendar and convert to Orgmode compatible plain text.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

216 lines
6.5 KiB

  1. # Copyright 2020 Harish Krupo
  2. # Licensed under the Apache License, Version 2.0 (the "License");
  3. # you may not use this file except in compliance with the License.
  4. # You may obtain a copy of the License at
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. # Unless required by applicable law or agreed to in writing, software
  7. # distributed under the License is distributed on an "AS IS" BASIS,
  8. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. # See the License for the specific language governing permissions and
  10. # limitations under the License.
  11. from xdg.BaseDirectory import (
  12. load_first_config,
  13. save_data_path
  14. )
  15. import argparse
  16. import webbrowser
  17. import logging
  18. import json
  19. import msal
  20. from http.server import HTTPServer, BaseHTTPRequestHandler
  21. from urllib.parse import urlparse, parse_qs, urlencode
  22. from wsgiref import simple_server
  23. import wsgiref.util
  24. import sys
  25. import uuid
  26. import pprint
  27. import os
  28. import atexit
  29. import base64
  30. import gnupg
  31. import io
  32. pp = pprint.PrettyPrinter(indent=4)
  33. credentials_file = save_data_path("oauth2ms") + "/credentials.bin"
  34. _LOGGER = logging.getLogger(__name__)
  35. APP_NAME = "oauth2ms"
  36. SUCCESS_MESSAGE = "Authorization complete."
  37. def load_config():
  38. config_file = load_first_config(APP_NAME, "config.json")
  39. if config_file is None:
  40. print(f"Couldn't find configuration file. Config file must be at: $XDG_CONFIG_HOME/{APP_NAME}/config.json")
  41. print("Current value of $XDG_CONFIG_HOME is {}".format(os.getenv("XDG_CONFIG_HOME")))
  42. return None
  43. return json.load(open(config_file, 'r'))
  44. def build_msal_app(config, cache = None):
  45. return msal.ConfidentialClientApplication(
  46. config['client_id'],
  47. authority="https://login.microsoftonline.com/" + config['tenant_id'],
  48. client_credential=config['client_secret'], token_cache=cache)
  49. def get_auth_url(config, cache, state):
  50. return build_msal_app(config, cache).get_authorization_request_url(
  51. config['scopes'],
  52. state=state,
  53. redirect_uri=config["redirect_uri"]);
  54. class WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler):
  55. """Silence out the messages from the http server"""
  56. def log_message(self, format, *args):
  57. pass
  58. class WSGIRedirectionApp(object):
  59. """WSGI app to handle the authorization redirect.
  60. Stores the request URI and displays the given success message.
  61. """
  62. def __init__(self, message):
  63. self.last_request_uri = None
  64. self._success_message = message
  65. def __call__(self, environ, start_response):
  66. start_response("200 OK", [("Content-type", "text/plain; charset=utf-8")])
  67. self.last_request_uri = wsgiref.util.request_uri(environ)
  68. return [self._success_message.encode("utf-8")]
  69. def validate_config(config):
  70. conditionsBoth = [
  71. "tenant_id" in config,
  72. "client_id" in config,
  73. "scopes" in config,
  74. ]
  75. conditionsAuth = [
  76. "redirect_host" in config,
  77. "redirect_port" in config,
  78. "redirect_path" in config,
  79. "client_secret" in config
  80. ]
  81. if not all(conditionsBoth):
  82. print("Invalid config")
  83. print("Config must contain the keys: " +
  84. "tenant_id, client_id, scopes");
  85. return False
  86. else:
  87. return True
  88. def build_new_app_state():
  89. cache = msal.SerializableTokenCache()
  90. config = load_config()
  91. if config is None:
  92. return None
  93. if not validate_config(config):
  94. return None
  95. state = str(uuid.uuid4())
  96. redirect_path = config["redirect_path"]
  97. # Remove / at the end if present, and path is not just /
  98. if redirect_path != "/" and redirect_path[-1] == "/":
  99. redirect_path = redirect_path[:-1]
  100. config["redirect_uri"] = "http://" + config['redirect_host'] + ":" + config['redirect_port'] + redirect_path
  101. auth_url = get_auth_url(config, cache, state)
  102. wsgi_app = WSGIRedirectionApp(SUCCESS_MESSAGE)
  103. http_server = simple_server.make_server(config['redirect_host'],
  104. int(config['redirect_port']),
  105. wsgi_app,
  106. handler_class=WSGIRequestHandler)
  107. webbrowser.open(auth_url, new=2, autoraise=True)
  108. http_server.handle_request()
  109. auth_response = wsgi_app.last_request_uri
  110. http_server.server_close()
  111. parsed_auth_response = parse_qs(auth_response)
  112. code_key = config["redirect_uri"] + "?code"
  113. if code_key not in parsed_auth_response:
  114. return None
  115. auth_code = parsed_auth_response[code_key]
  116. result = build_msal_app(config, cache).acquire_token_by_authorization_code(
  117. auth_code,
  118. scopes=config['scopes'],
  119. redirect_uri=config["redirect_uri"]);
  120. if result.get("access_token") is None:
  121. print("Something went wrong during authorization")
  122. print(f"Server returned: {result}")
  123. return None
  124. token = result["access_token"]
  125. app = {}
  126. app['config'] = config
  127. app['cache'] = cache
  128. return app, token
  129. def fetch_token_from_cache(app):
  130. config = app['config']
  131. cache = app['cache']
  132. cca = build_msal_app(config, cache)
  133. accounts = cca.get_accounts()
  134. if cca.get_accounts:
  135. result = cca.acquire_token_silent(config['scopes'], account=accounts[0])
  136. return result["access_token"]
  137. else:
  138. None
  139. def build_app_state_from_credentials():
  140. # If the file is missing return None
  141. if not os.path.exists(credentials_file):
  142. return None
  143. credentials = open(credentials_file, "r").read()
  144. # Make sure it is a valid json object
  145. try:
  146. config = json.loads(credentials)
  147. except:
  148. print("Not a valild json file or it is ecrypted. Maybe add/remove the -e arugment?")
  149. sys.exit(1)
  150. # We don't have a token cache?
  151. if config.get("token_cache") is None:
  152. return None
  153. app_state = {};
  154. cache = msal.SerializableTokenCache()
  155. cache.deserialize(config["token_cache"])
  156. app_state["config"] = config
  157. app_state["cache"] = cache
  158. return app_state
  159. token = None
  160. app_state = build_app_state_from_credentials()
  161. if app_state is None:
  162. app_state, token = build_new_app_state()
  163. if app_state is None:
  164. print("Something went wrong!")
  165. sys.exit(1)
  166. if token is None:
  167. token = fetch_token_from_cache(app_state)
  168. cache = app_state['cache']
  169. if cache.has_state_changed:
  170. config = app_state["config"]
  171. config["token_cache"] = cache.serialize()
  172. config_json = json.dumps(config)
  173. open(credentials_file, "w").write(config_json)