diff --git a/platypush/backend/http/static/js/widgets/calendar.js b/platypush/backend/http/static/js/widgets/calendar.js index 26926f54..9d1aa694 100644 --- a/platypush/backend/http/static/js/widgets/calendar.js +++ b/platypush/backend/http/static/js/widgets/calendar.js @@ -22,7 +22,7 @@ $(document).ready(function() { execute( { type: 'request', - action: 'google.calendar.get_upcoming_events', + action: 'calendar.get_upcoming_events', args: { max_results: 9, } @@ -30,6 +30,8 @@ $(document).ready(function() { onSuccess = function(response) { var events = response.response.output; + $eventsListContainer.html(''); + for (var i=0; i < events.length; i++) { var event = events[i]; var start = new Date('dateTime' in event.start ? event.start.dateTime : event.start.date); diff --git a/platypush/plugins/calendar/__init__.py b/platypush/plugins/calendar/__init__.py new file mode 100644 index 00000000..065bfd26 --- /dev/null +++ b/platypush/plugins/calendar/__init__.py @@ -0,0 +1,72 @@ +import dateutil.parser +import importlib +import logging + +from abc import ABCMeta, abstractmethod + +from platypush.plugins import Plugin +from platypush.message.response import Response + + +class CalendarInterface: + __metaclass__ = ABCMeta + + @abstractmethod + def get_upcoming_events(self, max_results=10): + raise NotImplementedError() + + +class CalendarPlugin(Plugin, CalendarInterface): + """ + The CalendarPlugin will allow you to keep track of multiple calendars + (Google or iCal URLs) and get joined events from all of them + """ + + def __init__(self, calendars=[], *args, **kwargs): + """ + Example calendars format: + + ``` + [ + { + "type": "platypush.plugins.google.calendar.GoogleCalendarPlugin" + }, + + { + "type": "platypush.plugins.calendar.ical.IcalCalendarPlugin", + "url": "https://www.facebook.com/ical/u.php?uid=USER_ID&key=FB_KEY" + }, + + ... + ] + """ + + super().__init__(*args, **kwargs) + self.calendars = [] + + for calendar in calendars: + if 'type' not in calendar: + logging.warning("Invalid calendar with no type specified: {}".format(calendar)) + continue + + cal_type = calendar.pop('type') + module_name = '.'.join(cal_type.split('.')[:-1]) + class_name = cal_type.split('.')[-1] + module = importlib.import_module(module_name) + self.calendars.append(getattr(module, class_name)(**calendar)) + + + def get_upcoming_events(self, max_results=10): + events = [] + + for calendar in self.calendars: + events.extend(calendar.get_upcoming_events().output) + + events = sorted(events, key=lambda event: + dateutil.parser.parse(event['start']['dateTime']))[:max_results] + + return Response(output=events) + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/plugins/calendar/ical.py b/platypush/plugins/calendar/ical.py new file mode 100644 index 00000000..ee8a4a8c --- /dev/null +++ b/platypush/plugins/calendar/ical.py @@ -0,0 +1,76 @@ +import datetime +import dateutil.parser +import requests +import pytz + +from icalendar import Calendar, Event + +from platypush.message.response import Response +from platypush.plugins import Plugin +from platypush.plugins.calendar import CalendarInterface + + +class IcalCalendarPlugin(Plugin, CalendarInterface): + def __init__(self, url, *args, **kwargs): + super().__init__(*args, **kwargs) + self.url = url + + + def _translate_event(self, event): + return { + 'id': str(event.get('uid')) if event.get('uid') else None, + 'kind': 'calendar#event', + 'summary': str(event.get('summary')) if event.get('summary') else None, + 'description': str(event.get('description')) if event.get('description') else None, + 'status': str(event.get('status')).lower() if event.get('status') else None, + 'responseStatus': str(event.get('partstat')).lower() if event.get('partstat') else None, + 'location': str(event.get('location')) if event.get('location') else None, + 'htmlLink': str(event.get('url')) if event.get('url') else None, + 'organizer': { + 'email': str(event.get('organizer')).replace('MAILTO:', ''), + 'displayName': event.get('organizer').params['cn'] + } if event.get('organizer') else None, + + 'created': event.get('created').dt.isoformat() if event.get('created') else None, + 'updated': event.get('last-modified').dt.isoformat() if event.get('last-modified') else None, + 'start': { + 'dateTime': event.get('dtstart').dt.isoformat() if event.get('dtstart') else None, + 'timeZone': 'UTC', # TODO Support multiple timezone IDs + }, + + 'end': { + 'dateTime': event.get('dtend').dt.isoformat() if event.get('dtend') else None, + 'timeZone': 'UTC', + }, + } + + + def get_upcoming_events(self, max_results=10, only_participating=True): + events = [] + response = requests.get(self.url) + + if response.ok: + calendar = Calendar.from_ical(response.text) + for event in calendar.walk(): + if event.name != 'VEVENT': + continue # Not an event + + event = self._translate_event(event) + + if event['status'] and event['responseStatus'] \ + and dateutil.parser.parse(event['end']['dateTime']) >= \ + datetime.datetime.now(pytz.timezone('UTC')) \ + and ( + (only_participating + and event['status'] == 'confirmed' + and event['responseStatus'] in ['accepted', 'tentative']) + or not only_participating): + events.append(event) + else: + logging.error("HTTP error while getting {}: {}".format(self.url, response)) + + return Response(output=events) + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/plugins/google/calendar.py b/platypush/plugins/google/calendar.py index d66f244d..4d6881b4 100644 --- a/platypush/plugins/google/calendar.py +++ b/platypush/plugins/google/calendar.py @@ -7,9 +7,10 @@ from apiclient import discovery from platypush.message.response import Response from platypush.plugins.google import GooglePlugin +from platypush.plugins.calendar import CalendarInterface -class GoogleCalendarPlugin(GooglePlugin): +class GoogleCalendarPlugin(GooglePlugin, CalendarInterface): scopes = ['https://www.googleapis.com/auth/calendar.readonly'] def __init__(self, *args, **kwargs): diff --git a/setup.py b/setup.py index c512552f..641d1f2d 100755 --- a/setup.py +++ b/setup.py @@ -85,6 +85,7 @@ setup( 'Support for GPIO pins access': ['RPi.GPIO'], 'Support for MCP3008 analog-to-digital converter plugin': ['adafruit-mcp3008'], 'Support for smart cards detection': ['pyscard'], + 'Support for ICal calendars': ['icalendar', 'python-dateutil'], # 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git', 'redis'], # 'Support for Flic buttons': ['git+ssh://git@github.com/50ButtonsEach/fliclib-linux-hci'] },