Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

169 lines
7.4KB

  1. # Copyright (C) 2019 Campaign Against Arms Trade
  2. #
  3. # This program is free software: you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation, either version 3 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. # You should have received a copy of the GNU General Public License
  13. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. """Handles all api interaction with Office365"""
  15. import os
  16. from os.path import dirname, isfile, realpath
  17. from datetime import datetime, timedelta
  18. import O365
  19. from requests import HTTPError
  20. from . import ACCESS_TOKEN, STRFTIME, STRPTIME, LOG, dates
  21. class Account(O365.Account):
  22. """Wrapper for the O365 Account class to add our api interactions"""
  23. @classmethod
  24. def create_session(cls, credentials, tenant_id):
  25. """
  26. Create a session with the API and save the token for later use.
  27. :param credentials: tuple of (client_id, client_secret)
  28. :param tentant_id: str of tenant_id
  29. :return: Account class and email: str
  30. """
  31. my_protocol = O365.MSOffice365Protocol(api_version='v2.0')
  32. token_backend = O365.FileSystemTokenBackend(token_filename=ACCESS_TOKEN)
  33. return cls(
  34. credentials,
  35. protocol=my_protocol,
  36. tenant_id=tenant_id,
  37. token_backend=token_backend,
  38. raise_http_error=False
  39. )
  40. def authenticate_session(self):
  41. """
  42. Authenticates account session object with oauth.
  43. Uses the default auth flow that comes with the library
  44. :param session: Account object
  45. :return: Bool for if the client is authenticated
  46. """
  47. if not isfile(f"{dirname(realpath(__file__))}/access_token"):
  48. try:
  49. self.authenticate(scopes=['basic', 'address_book', 'users', 'calendar_shared'])
  50. self.con.oauth_request("https://outlook.office.com/api/v2.0/me/", "get")
  51. LOG.debug("Successfully tested new access_token")
  52. return True
  53. except:
  54. os.remove(ACCESS_TOKEN)
  55. LOG.error("Could not authenticate. Make sure config has the correct client_id, client_secret, etc. Exiting...")
  56. exit(1)
  57. else:
  58. try:
  59. self.con.oauth_request("https://outlook.office.com/api/v2.0/me/", "get")
  60. LOG.debug("Successfully tested current access_token")
  61. return True
  62. except:
  63. os.remove(ACCESS_TOKEN)
  64. LOG.warning("Failed to authenticate with current access_token. Deleting access_token and running authentication again.")
  65. return False
  66. def get_event_range(self, beginning_of_week: datetime, email: str):
  67. """
  68. Makes call to grab calender view within a 2 week window either side of the current week.
  69. :param beginning_of_week: datetime object for this weeks monday
  70. :return: dict of json response
  71. """
  72. base_url = "https://outlook.office.com/api/v2.0/"
  73. scope = f"users/{email}/CalendarView"
  74. # Create a range of dates for this week so we can catch long events within the search
  75. bottom_range = beginning_of_week - timedelta(days=14)
  76. top_range = beginning_of_week + timedelta(days=21)
  77. # Setup url
  78. date_range = "startDateTime={}&endDateTime={}".format(bottom_range.strftime(STRFTIME), top_range.strftime(STRPTIME))
  79. limit = "$top=150"
  80. select = "$select=Subject,Organizer,Start,End,Attendees"
  81. url = f"{base_url}{scope}?{date_range}&{select}&{limit}"
  82. resp = self.con.oauth_request(url, "get")
  83. return resp.json()
  84. def get_ooo_list(self, email: str, week_no: int):
  85. """
  86. Makes request and parses data into a list of users who will not be in the office
  87. :param email: string of the outofoffice email where the out of office calender is located
  88. :param week_no: week number. 1 = Current week, n > 1 = current week + n weeks
  89. :return: list of 5 lists representing 5 days. Each contains lowercase names of who is not in the office.
  90. """
  91. monday, friday = dates.get_week_datetime(week_no)
  92. try:
  93. events = self.get_event_range(monday, email)
  94. LOG.debug("Received response for two week range from week beginning with {:%Y-%m-%d} from outofoffice account with email: {}".format(monday, email))
  95. except HTTPError as error:
  96. LOG.error("Could not request CalendarView | %s", error.response)
  97. exit(1)
  98. events = events["value"]
  99. outofoffice = [[], [], [], [], []]
  100. for event in events:
  101. # removes last char due to microsoft's datetime using 7 digits
  102. # for microseconds, python uses 6
  103. start = datetime.strptime(event["Start"]["DateTime"][:-1], STRPTIME)
  104. end = datetime.strptime(event["End"]["DateTime"][:-1], STRPTIME)
  105. attendees = event["Attendees"]
  106. # remove outofoffice account by list comprehension
  107. attendees = [x for x in attendees if x["EmailAddress"]["Address"] != email]
  108. organizer = event["Organizer"]
  109. if not attendees and organizer["EmailAddress"]["Address"] != email:
  110. # Sometimes user will be the one who makes the event. Get the organizer.
  111. attendees = [event["Organizer"]]
  112. if (end - start) <= timedelta(days=1):
  113. # Event is for one day only, check if it starts within the week
  114. if monday <= start <= friday:
  115. # Event is within the week we are looking at, add all attendees
  116. weekday = outofoffice[start.weekday()]
  117. if not attendees:
  118. LOG.warning(
  119. "Event '%s' has no attendees. Cannot add to outofoffice list.",
  120. event["Subject"]
  121. )
  122. weekday = self.add_attendees_to_ooo_list(attendees, weekday)
  123. else:
  124. # Check if long events cover the days of this week
  125. for i, day_array in enumerate(outofoffice.copy()):
  126. current_day = monday + timedelta(days=i)
  127. if start <= current_day <= end:
  128. # if day is inside of the long event
  129. outofoffice[i] = self.add_attendees_to_ooo_list(attendees, day_array)
  130. LOG.debug("Parsed events and successfully created out of office list.")
  131. return outofoffice
  132. @staticmethod
  133. def add_attendees_to_ooo_list(attendees: list, ooo_list: list):
  134. """
  135. Adds attendees to ooo_list from api list of attendees
  136. :param attendees: list of attendees to event
  137. :param ooo_list: list of the current days out of office users
  138. :return: ooo_list once appended
  139. """
  140. for attendee in attendees:
  141. attendee_name = attendee["EmailAddress"]["Name"].split(" ")[0] # Get first name
  142. if attendee_name not in ooo_list.copy():
  143. # Converted to lowercase so program is case insensitive
  144. ooo_list.append(attendee_name.lower())
  145. return ooo_list