diff --git a/O365/calendar.py b/O365/calendar.py index 2f65bd185cfe..5f899913f3c0 100644 --- a/O365/calendar.py +++ b/O365/calendar.py @@ -788,7 +788,7 @@ def __init__(self, *, parent=None, con=None, **kwargs): """ Create a calendar event representation :param parent: parent for this operation - :type parent: Calendar or Schedule + :type parent: Calendar or Schedule or ApiComponent :param Connection con: connection to use if no parent specified :param Protocol protocol: protocol to use if no parent specified (kwargs) diff --git a/O365/connection.py b/O365/connection.py index c52bcca6dce7..fbdc85850093 100644 --- a/O365/connection.py +++ b/O365/connection.py @@ -204,6 +204,7 @@ def __init__(self, api_version='v1.0', default_resource=ME_RESOURCE, **kwargs) self.keyword_data_store['message_type'] = 'microsoft.graph.message' + self.keyword_data_store['event_message_type'] = 'microsoft.graph.eventMessage' self.keyword_data_store[ 'file_attachment_type'] = '#microsoft.graph.fileAttachment' self.keyword_data_store[ @@ -241,6 +242,8 @@ def __init__(self, api_version='v2.0', default_resource=ME_RESOURCE, self.keyword_data_store[ 'message_type'] = 'Microsoft.OutlookServices.Message' + self.keyword_data_store[ + 'event_message_type'] = 'Microsoft.OutlookServices.EventMessage' self.keyword_data_store[ 'file_attachment_type'] = '#Microsoft.OutlookServices.' \ 'FileAttachment' diff --git a/O365/message.py b/O365/message.py index 66abdec08d6e..876d81127d84 100644 --- a/O365/message.py +++ b/O365/message.py @@ -10,10 +10,20 @@ from .utils import OutlookWellKnowFolderNames, ApiComponent, \ BaseAttachments, BaseAttachment, AttachableMixin, ImportanceLevel, \ TrackerSet, Recipient, HandleRecipientsMixin +from .calendar import Event log = logging.getLogger(__name__) +class MeetingMessageType(Enum): + NoneMessageType = 'none' + MeetingRequest = 'meetingRequest' + MeetingCancelled = 'meetingCancelled' + MeetingAccepted = 'meetingAccepted' + MeetingTentativelyAccepted = 'meetingTentativelyAccepted' + MeetingDeclined = 'meetingDeclined' + + class MessageAttachment(BaseAttachment): _endpoints = { 'attach': '/messages/{id}/attachments', @@ -247,7 +257,10 @@ def __init__(self, *, parent=None, con=None, **kwargs): self.__is_read_receipt_requested = cloud_data.get(cc('isReadReceiptRequested'), False) self.__is_delivery_receipt_requested = cloud_data.get(cc('isDeliveryReceiptRequested'), False) - # A message is a draft by default + # if this message is an EventMessage: + self.__meeting_message_type = MeetingMessageType(cloud_data.get(cc('meetingMessageType'), 'none')) + + # a message is a draft by default self.__is_draft = cloud_data.get(cc('isDraft'), kwargs.get('is_draft', True)) self.conversation_id = cloud_data.get(cc('conversationId'), None) @@ -256,6 +269,12 @@ def __init__(self, *, parent=None, con=None, **kwargs): flag_data = cloud_data.get(cc('flag'), {}) self.__flag = MessageFlag(parent=self, flag_data=flag_data) + def __str__(self): + return self.__repr__() + + def __repr__(self): + return 'Subject: {}'.format(self.subject) + def _clear_tracker(self): # reset the tracked changes. Usually after a server update self._track_changes = TrackerSet(casing=self._cc) @@ -460,6 +479,21 @@ def is_delivery_receipt_requested(self, value): self.__is_delivery_receipt_requested = bool(value) self._track_changes.add('isDeliveryReceiptRequested') + @property + def meeting_message_type(self): + """ If this message is a EventMessage, returns the + meeting type: meetingRequest, meetingCancelled, meetingAccepted, + meetingTentativelyAccepted, meetingDeclined + """ + return self.__meeting_message_type + + @property + def is_event_message(self): + """ Returns if this message is of type EventMessage + and therefore can return the related event. + """ + return self.__meeting_message_type is not MeetingMessageType.NoneMessageType + @property def flag(self): """ The Message Flag instance """ @@ -868,8 +902,23 @@ def get_body_soup(self): else: return bs(self.body, 'html.parser') - def __str__(self): - return self.__repr__() + def get_event(self): + """ If this is a EventMessage it should return the related Event""" - def __repr__(self): - return 'Subject: {}'.format(self.subject) + if not self.is_event_message: + return None + + # select a dummy field (eg. subject) to avoid pull unneccesary data + query = self.q().select('subject').expand('event') + + url = self.build_url(self._endpoints.get('get_message').format(id=self.object_id)) + + response = self.con.get(url, params=query.as_params()) + + if not response: + return None + + data = response.json() + event_data = data.get(self._cc('event')) + + return Event(parent=self, **{self._cloud_data_key: event_data}) diff --git a/O365/utils/utils.py b/O365/utils/utils.py index b2f10ceb6bc5..7e1fceafca06 100644 --- a/O365/utils/utils.py +++ b/O365/utils/utils.py @@ -547,11 +547,13 @@ def __init__(self, attribute=None, *, protocol): self._filters = [] # store all the filters self._order_by = OrderedDict() self._selects = set() + self._expands = set() def __str__(self): - return 'Filter: {}\nOrder: {}\nSelect: {}'.format(self.get_filters(), - self.get_order(), - self.get_selects()) + return 'Filter: {}\nOrder: {}\nSelect: {}\nExpand: {}'.format(self.get_filters(), + self.get_order(), + self.get_selects(), + self.get_expands()) def __repr__(self): return self.__str__() @@ -580,6 +582,24 @@ def select(self, *attributes): return self + @fluent + def expand(self, *relationships): + """ Adds the relationships (e.g. "event" or "attachments") + that should be expanded with the $expand parameter + Important: The ApiComponent using this should know how to handle this relationships. + eg: Message knows how to handle attachments, and event (if it's an EventMessage). + Important: When using expand on multi-value relationships a max of 20 items will be returned. + :param str relationships: the relationships tuple to expand. + :rtype: Query + """ + + for relationship in relationships: + if relationship == 'event': + relationship = '{}/event'.format(self.protocol.get_service_keyword('event_message_type')) + self._expands.add(relationship) + + return self + def as_params(self): """ Returns the filters and orders as query parameters @@ -592,6 +612,8 @@ def as_params(self): params['$orderby'] = self.get_order() if self.has_selects: params['$select'] = self.get_selects() + if self.has_expands: + params['$expand'] = self.get_expands() return params @property @@ -618,6 +640,14 @@ def has_selects(self): """ return bool(self._selects) + @property + def has_expands(self): + """ Whether the query has relationships that should be expanded or not + + :rtype: bool + """ + return bool(self._expands) + def get_filters(self): """ Returns the result filters @@ -674,6 +704,16 @@ def get_selects(self): else: return None + def get_expands(self): + """ Returns the result expand clause + + :rtype: str or None + """ + if self._expands: + return ','.join(self._expands) + else: + return None + def _get_mapping(self, attribute): if attribute: mapping = self._mapping.get(attribute) @@ -830,7 +870,7 @@ def logical_operator(self, operation, word): word = self._parse_filter_word(word) self._add_filter( *self._prepare_sentence(self._attribute, operation, word, - self._negation)) + self._negation)) return self @fluent @@ -905,7 +945,7 @@ def function(self, function_name, word): self._add_filter( *self._prepare_function(function_name, self._attribute, word, - self._negation)) + self._negation)) return self @fluent