def parse_event(self, event, extra): try: uid = str(event['guid']) # The entirety of the raw event data in json representation. raw_data = str(event) subject = event.get('title', '')[:SUBJECT_MAX_LEN] body = event.get('description', None) location = event.get('location', None) if location: location = location[:LOCATION_MAX_LEN] all_day = event.get('allDay', False) read_only = event.get('readOnly') # for some reason iCloud gives the date as YYYYMMDD for the first # entry and then the Y, M, D, H, S as later entries. start_date = event['startDate'][1:] end_date = event['endDate'][1:] start = datetime.datetime(*start_date[:-1]) end = datetime.datetime(*end_date[:-1]) recurrence = event['recurrence'] # iCloud doesn't give us busy information busy = True # reminder format is super-funky, punt for now -cg3 reminders = str([]) # and for some reason iCloud isn't giving us participants participants = [] except (KeyError, AttributeError): raise MalformedEventError() return Event(account_id=self.account_id, uid=uid, provider_name=self.PROVIDER_NAME, raw_data=raw_data, subject=subject, body=body, location=location, reminders=reminders, recurrence=recurrence, start=start, end=end, busy=busy, all_day=all_day, read_only=read_only, source='remote', is_owner=True, participants=participants)
def parse_event(self, event, cal_info): """Constructs an Event object from a Google calendar entry. Parameters ---------- event: gdata.calendar.entry.CalendarEntry The Google calendar entry to parse. Returns ------- ..models.tables.base.Event A corresponding Inbox Event instance. Raises ------ MalformedEventError If the calendar data could not be parsed correctly. """ try: uid = str(event['id']) # The entirety of the raw event data in json representation. raw_data = str(event) # 'cancelled' events signify those instances within a series # that have been cancelled (for that given series). As such, # since full support for dealing with single instances within # a reocurring event series is not added, right now we just # treat this event as 'malformed'. -cg3 # TODO: Add support for reocurring events (see ways to handle # this generically across providers) if 'status' in event and event['status'] == 'cancelled': raise MalformedEventError() subject = event.get('summary', '')[:SUBJECT_MAX_LEN] body = event.get('description', None) location = event.get('location', None) if location: location = location[:LOCATION_MAX_LEN] all_day = False read_only = True is_owner = False start = event['start'] end = event['end'] g_reccur = event.get('recurrence', None) recurrence = str(g_reccur) if g_reccur else None busy = event.get('transparency', True) if busy == 'transparent': busy = False reminders = [] if 'dateTime' in start: if event['reminders']['useDefault']: reminder_source = cal_info['defaultReminders'] elif 'overrides' in event['reminders']: reminder_source = event['reminders']['overrides'] else: reminder_source = None if reminder_source: for reminder in reminder_source: reminders.append(reminder['minutes']) start = parse_datetime(start['dateTime']) end = parse_datetime(end['dateTime']) else: start = date_parser.parse(start['date']) end = date_parser.parse(end['date']) all_day = True reminders = str(reminders) # Convert google's notion of status into our own participants = [] status_map = {'accepted': 'yes', 'needsAction': 'noreply', 'declined': 'no', 'tentative': 'maybe'} for attendee in event.get('attendees', []): g_status = attendee.get('responseStatus') if g_status not in status_map: raise MalformedEventError() status = status_map[g_status] email = attendee.get('email') if not email: raise MalformedEventError() name = attendee.get('displayName') notes = None if 'additionalGuests' in attendee: notes = "Guests: {}".format(attendee['additionalGuests']) if 'comment' in attendee: notes += " Notes: {}".format(attendee['comment']) elif 'comment' in attendee: notes = "Notes: {}".format(attendee['comment']) participants.append(Participant(email_address=email, name=name, status=status, notes=notes)) if 'self' in event['creator']: is_owner = True read_only = False elif 'guestsCanModify' in event: read_only = False owner = "{} <{}>".format(event['creator']['displayName'], event['creator']['email']) except (KeyError, AttributeError): raise MalformedEventError() return Event(account_id=self.account_id, uid=uid, provider_name=self.PROVIDER_NAME, raw_data=raw_data, subject=subject, body=body, location=location, reminders=reminders, recurrence=recurrence, start=start, end=end, owner=owner, is_owner=is_owner, busy=busy, all_day=all_day, read_only=read_only, source='remote', participants=participants)
def events_from_ics(namespace, calendar, ics_str): try: cal = iCalendar.from_ical(ics_str) except (ValueError, IndexError, KeyError): raise MalformedEventError() events = [] # See: https://tools.ietf.org/html/rfc5546#section-3.2 calendar_method = None for component in cal.walk(): if component.name == "VCALENDAR": calendar_method = component.get('method') if component.name == "VTIMEZONE": tzname = component.get('TZID') assert tzname in timezones_table,\ "Non-UTC timezone should be in table" if component.name == "VEVENT": # Make sure the times are in UTC. try: original_start = component.get('dtstart').dt original_end = component.get('dtend').dt except AttributeError: raise MalformedEventError("Event lacks start and/or end time") start = original_start end = original_end original_start_tz = None if isinstance(start, datetime) and isinstance(end, datetime): all_day = False original_start_tz = str(original_start.tzinfo) # icalendar doesn't parse Windows timezones yet # (see: https://github.com/collective/icalendar/issues/44) # so we look if the timezone isn't in our Windows-TZ # to Olson-TZ table. if original_start.tzinfo is None: tzid = component.get('dtstart').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] original_start_tz = corresponding_tz local_timezone = pytz.timezone(corresponding_tz) start = local_timezone.localize(original_start) if original_end.tzinfo is None: tzid = component.get('dtend').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] local_timezone = pytz.timezone(corresponding_tz) end = local_timezone.localize(original_end) elif isinstance(start, date) and isinstance(end, date): all_day = True start = arrow.get(start) end = arrow.get(end) # Get the last modification date. # Exchange uses DtStamp, iCloud and Gmail LAST-MODIFIED. last_modified_tstamp = component.get('dtstamp') last_modified = None if last_modified_tstamp is not None: # This is one surprising instance of Exchange doing # the right thing by giving us an UTC timestamp. Also note that # Google calendar also include the DtStamp field, probably to # be a good citizen. if last_modified_tstamp.dt.tzinfo is not None: last_modified = last_modified_tstamp.dt else: raise NotImplementedError("We don't support arcane Windows" " timezones in timestamps yet") else: # Try to look for a LAST-MODIFIED element instead. # Note: LAST-MODIFIED is always in UTC. # http://www.kanzaki.com/docs/ical/lastModified.html last_modified = component.get('last-modified').dt assert last_modified is not None, \ "Event should have a DtStamp or LAST-MODIFIED timestamp" title = None summaries = component.get('summary', []) if not isinstance(summaries, list): summaries = [summaries] if summaries != []: title = " - ".join(summaries) description = unicode(component.get('description')) event_status = component.get('status') if event_status is not None: event_status = event_status.lower() else: # Some providers (e.g: iCloud) don't use the status field. # Instead they use the METHOD field to signal cancellations. method = component.get('method') if method and method.lower() == 'cancel': event_status = 'cancelled' elif calendar_method and calendar_method.lower() == 'cancel': # So, this particular event was not cancelled. Maybe the # whole calendar was. event_status = 'cancelled' else: # Otherwise assume the event has been confirmed. event_status = 'confirmed' assert event_status in EVENT_STATUSES recur = component.get('rrule') if recur: recur = "RRULE:{}".format(recur.to_ical()) participants = [] organizer = component.get('organizer') if organizer: # Here's the problem. Gmail and Exchange define the organizer # field like this: # # ORGANIZER;CN="User";EMAIL="*****@*****.**":mailto:[email protected] # but iCloud does it like this: # ORGANIZER;CN=User;[email protected]:mailto: # [email protected] # so what we first try to get the EMAIL field, and only if # it's not present we use the MAILTO: link. if 'EMAIL' in organizer.params: organizer = organizer.params['EMAIL'] else: organizer = unicode(organizer) if organizer.startswith('mailto:'): organizer = organizer[7:] if (namespace.account.email_address == canonicalize_address( organizer)): is_owner = True else: is_owner = False attendees = component.get('attendee', []) # the iCalendar python module doesn't return a list when # there's only one attendee. Go figure. if not isinstance(attendees, list): attendees = [attendees] for attendee in attendees: email = unicode(attendee) # strip mailto: if it exists if email.lower().startswith('mailto:'): email = email[7:] try: name = attendee.params['CN'] except KeyError: name = None status_map = { 'NEEDS-ACTION': 'noreply', 'ACCEPTED': 'yes', 'DECLINED': 'no', 'TENTATIVE': 'maybe' } status = 'noreply' try: a_status = attendee.params['PARTSTAT'] status = status_map[a_status] except KeyError: pass notes = None try: guests = attendee.params['X-NUM-GUESTS'] notes = "Guests: {}".format(guests) except KeyError: pass participants.append({ 'email': email, 'name': name, 'status': status, 'notes': notes, 'guests': [] }) location = component.get('location') uid = str(component.get('uid')) event = Event(namespace=namespace, calendar=calendar, uid=uid, provider_name='ics', raw_data=component.to_ical(), title=title, description=description, location=location, reminders=str([]), recurrence=recur, start=start, end=end, busy=True, all_day=all_day, read_only=True, is_owner=is_owner, last_modified=last_modified, original_start_tz=original_start_tz, source='local', status=event_status, participants=participants) events.append(event) return events
def events_from_ics(namespace, calendar, ics_str): try: cal = iCalendar.from_ical(ics_str) except (ValueError, IndexError, KeyError): raise MalformedEventError() events = dict(invites=[], rsvps=[]) # See: https://tools.ietf.org/html/rfc5546#section-3.2 calendar_method = None for component in cal.walk(): if component.name == "VCALENDAR": calendar_method = component.get("method") if component.name == "VTIMEZONE": tzname = component.get("TZID") assert tzname in timezones_table, "Non-UTC timezone should be in table" if component.name == "VEVENT": # Make sure the times are in UTC. try: original_start = component.get("dtstart").dt original_end = component.get("dtend").dt except AttributeError: raise MalformedEventError("Event lacks start and/or end time") start = original_start end = original_end original_start_tz = None all_day = False if isinstance(start, datetime) and isinstance(end, datetime): tzid = str(original_start.tzinfo) if tzid in timezones_table: original_start_tz = timezones_table[tzid] if original_start.tzinfo is None: tzid = component.get("dtstart").params.get("TZID", None) assert (tzid in timezones_table ), "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] original_start_tz = corresponding_tz local_timezone = pytz.timezone(corresponding_tz) original_start = local_timezone.localize(original_start) if original_end.tzinfo is None: tzid = component.get("dtend").params.get("TZID", None) assert (tzid in timezones_table ), "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] local_timezone = pytz.timezone(corresponding_tz) original_end = local_timezone.localize(original_end) # Now that we have tz-aware datetimes, convert them to UTC start = original_start.astimezone(pytz.UTC) end = original_end.astimezone(pytz.UTC) elif isinstance(start, date) and isinstance(end, date): all_day = True start = arrow.get(start) end = arrow.get(end) assert isinstance(start, type(end)), ("Start and end should be of " "the same type") # Get the last modification date. # Exchange uses DtStamp, iCloud and Gmail LAST-MODIFIED. component_dtstamp = component.get("dtstamp") component_last_modified = component.get("last-modified") last_modified = None if component_dtstamp is not None: # This is one surprising instance of Exchange doing # the right thing by giving us an UTC timestamp. Also note that # Google calendar also include the DtStamp field, probably to # be a good citizen. if component_dtstamp.dt.tzinfo is not None: last_modified = component_dtstamp.dt else: raise NotImplementedError("We don't support arcane Windows" " timezones in timestamps yet") elif component_last_modified is not None: # Try to look for a LAST-MODIFIED element instead. # Note: LAST-MODIFIED is always in UTC. # http://www.kanzaki.com/docs/ical/lastModified.html last_modified = component_last_modified.dt title = None summaries = component.get("summary", []) if not isinstance(summaries, list): summaries = [summaries] if summaries != []: title = " - ".join(summaries) description = component.get("description") if description is not None: description = unicode(description) event_status = component.get("status") if event_status is not None: event_status = event_status.lower() else: # Some providers (e.g: iCloud) don't use the status field. # Instead they use the METHOD field to signal cancellations. method = component.get("method") if method and method.lower() == "cancel": event_status = "cancelled" elif calendar_method and calendar_method.lower() == "cancel": # So, this particular event was not cancelled. Maybe the # whole calendar was. event_status = "cancelled" else: # Otherwise assume the event has been confirmed. event_status = "confirmed" assert event_status in EVENT_STATUSES recur = component.get("rrule") if recur: recur = "RRULE:{}".format(recur.to_ical()) participants = [] organizer = component.get("organizer") organizer_name = None organizer_email = None if organizer: organizer_email = unicode(organizer) if organizer_email.lower().startswith("mailto:"): organizer_email = organizer_email[7:] if "CN" in organizer.params: organizer_name = organizer.params["CN"] owner = formataddr([organizer_name, organizer_email.lower()]) else: owner = None is_owner = False if owner is not None and ( namespace.account.email_address == canonicalize_address(organizer_email)): is_owner = True attendees = component.get("attendee", []) # the iCalendar python module doesn't return a list when # there's only one attendee. Go figure. if not isinstance(attendees, list): attendees = [attendees] for attendee in attendees: email = unicode(attendee) # strip mailto: if it exists if email.lower().startswith("mailto:"): email = email[7:] try: name = attendee.params["CN"] except KeyError: name = None status_map = { "NEEDS-ACTION": "noreply", "ACCEPTED": "yes", "DECLINED": "no", "TENTATIVE": "maybe", } status = "noreply" try: a_status = attendee.params["PARTSTAT"] status = status_map[a_status] except KeyError: pass notes = None try: guests = attendee.params["X-NUM-GUESTS"] notes = u"Guests: {}".format(guests) except KeyError: pass participants.append({ "email": email.lower(), "name": name, "status": status, "notes": notes, "guests": [], }) location = component.get("location") uid = str(component.get("uid")) sequence_number = int(component.get("sequence", 0)) # Some services (I'm looking at you, http://www.foogi.me/) # don't follow the spec and generate icalendar files with # ridiculously big sequence numbers. Truncate them to fit in # our db. if sequence_number > 2147483647: sequence_number = 2147483647 event = Event( namespace=namespace, calendar=calendar, uid=uid, provider_name="ics", raw_data=component.to_ical(), title=title, description=description, location=location, reminders=str([]), recurrence=recur, start=start, end=end, busy=True, all_day=all_day, read_only=True, owner=owner, is_owner=is_owner, last_modified=last_modified, original_start_tz=original_start_tz, source="local", status=event_status, sequence_number=sequence_number, participants=participants, ) # We need to distinguish between invites/updates/cancellations # and RSVPs. if calendar_method == "REQUEST" or calendar_method == "CANCEL": events["invites"].append(event) elif calendar_method == "REPLY": events["rsvps"].append(event) return events
def events_from_ics(namespace, calendar, ics_str): try: cal = Calendar.from_ical(ics_str) except ValueError: raise MalformedEventError() events = [] for component in cal.walk(): if component.name == "VEVENT": start = component.get('dtstart').dt end = component.get('dtend').dt title = component.get('summary') description = str(component.get('description')) if isinstance(start, datetime): all_day = False else: all_day = True start = datetime.combine(start, datetime.min.time()) end = datetime.combine(end, datetime.min.time()) reccur = component.get('rrule') if reccur: reccur = reccur.to_ical() else: reccur = '' participants = [] for attendee in component.get('attendee'): email = str(attendee) # strip mailto: if it exists if email.startswith('mailto:'): email = email[7:] try: name = attendee.params['CN'] except KeyError: name = None status_map = { 'NEEDS-ACTION': 'noreply', 'ACCEPTED': 'yes', 'DECLINED': 'no', 'TENTATIVE': 'maybe' } status = 'noreply' try: a_status = attendee.params['PARTSTAT'] status = status_map[a_status] except KeyError: pass notes = None try: guests = attendee.params['X-NUM-GUESTS'] notes = "Guests: {}".format(guests) except KeyError: pass participant = Participant(email_address=email, status=status, name=name, notes=notes) participants.append(participant) location = component.get('location') organizer = component.get('organizer') if (organizer): organizer = str(organizer) if organizer.startswith('mailto:'): organizer = organizer[7:] uid = str(component.get('uid')) event = Event(namespace=namespace, calendar=calendar, uid=uid, provider_name='ics', raw_data=component.to_ical(), title=title, description=description, location=location, reminders=str([]), recurrence=reccur, start=start, end=end, busy=True, all_day=all_day, read_only=True, source='local') event.participants = participants events.append(event) return events
def parse_event(self, event, cal_info): """Constructs an Event object from a Google calendar entry. Parameters ---------- event: gdata.calendar.entry.CalendarEntry The Google calendar entry to parse. Returns ------- ..models.tables.base.Event A corresponding Inbox Event instance. Raises ------ MalformedEventError If the calendar data could not be parsed correctly. """ try: uid = str(event['id']) # The entirety of the raw event data in json representation. raw_data = str(event) # 'cancelled' events signify those instances within a series # that have been cancelled (for that given series). As such, # since full support for dealing with single instances within # a reocurring event series is not added, right now we just # ignore the event. -cg3 # TODO: Add support for reocurring events (see ways to handle # this generically across providers) if 'status' in event and event['status'] == 'cancelled': return None title = event.get('summary', '') description = event.get('description', None) location = event.get('location', None) all_day = False read_only = True is_owner = False start = event['start'] end = event['end'] g_recur = event.get('recurrence', None) recurrence = str(g_recur) if g_recur else None busy = event.get('transparency', True) if busy == 'transparent': busy = False reminders = [] if 'dateTime' in start: if event['reminders']['useDefault']: reminder_source = cal_info['defaultReminders'] elif 'overrides' in event['reminders']: reminder_source = event['reminders']['overrides'] else: reminder_source = None if reminder_source: for reminder in reminder_source: reminders.append(reminder['minutes']) try: start = parse_datetime(start['dateTime']) end = parse_datetime(end['dateTime']) except TypeError: self.log.error('Invalid start: {} or end: {}'.format( start['dateTime'], end['dateTime'])) raise MalformedEventError() else: start = date_parser.parse(start['date']) end = date_parser.parse(end['date']) all_day = True reminders = str(reminders) # Convert google's notion of status into our own participants = [] for attendee in event.get('attendees', []): g_status = attendee.get('responseStatus') if g_status not in GoogleEventsProvider.status_map: raise MalformedEventError() status = GoogleEventsProvider.status_map[g_status] email = attendee.get('email') if not email: raise MalformedEventError() name = attendee.get('displayName') notes = None guests = 0 if 'additionalGuests' in attendee: guests = attendee['additionalGuests'] elif 'comment' in attendee: notes = attendee['comment'] participants.append({ 'email_address': email, 'name': name, 'status': status, 'notes': notes, 'guests': guests }) if 'guestsCanModify' in event: read_only = False owner = '' if 'creator' in event: creator = event['creator'] if 'self' in creator: is_owner = True read_only = False owner = u'{} <{}>'.format(creator.get('displayName', ''), creator.get('email', '')) except (KeyError, AttributeError): raise MalformedEventError() return Event(namespace_id=self.namespace_id, uid=uid, provider_name=self.PROVIDER_NAME, raw_data=raw_data, title=title, description=description, location=location, reminders=reminders, recurrence=recurrence, start=start, end=end, owner=owner, is_owner=is_owner, busy=busy, all_day=all_day, read_only=read_only, source='remote', participants=participants)
def parse_event(self, event, extra): user_id = extra['user_id'] stored_uids = extra['stored_uids'] try: uid = str(event['id']) if uid in stored_uids: raise MalformedEventError() # The entirety of the raw event data in json representation. raw_data = str(event) title = event.get('name', '') description = event.get('description', None) location = event.get('location', None) all_day = event.get('is_all_day_event', False) read_only = True is_owner = False owner = None start = parse_datetime(event['start_time']) end = parse_datetime(event['end_time']) # See if we made the event if 'from' in event['from']: if event['from'].get('id') == user_id: is_owner = True read_only = False else: is_owner = False owner = event['from'].get('name') recurrence = event['recurrence'] if event['is_recurrent'] else None busy = event['availability'] == 'busy' reminder_time = event.get('reminder_time') reminders = str([reminder_time] if reminder_time else []) participants = [] except (KeyError, AttributeError): raise MalformedEventError() stored_uids.append(uid) return Event(namespace_id=self.namespace_id, uid=uid, provider_name=self.PROVIDER_NAME, raw_data=raw_data, title=title, description=description, location=location, reminders=reminders, recurrence=recurrence, start=start, end=end, busy=busy, all_day=all_day, read_only=read_only, is_owner=is_owner, owner=owner, source='remote', participants=participants)
def parse_event(self, event, extra): if event is None: raise MalformedEventError() return event