from apiclient.channel import new_webhook_channel, Channel from django.db import models from eventplanner.models import Event, EventParticipation from apiclient.discovery import build from apiclient.http import BatchHttpRequest from oauth2client.file import Storage import httplib2 from django.conf import settings import logging import datetime from django.contrib.auth.models import User import time import uuid logger = logging.getLogger(__name__) class GCalMapping( models.Model ): gcal_id = models.CharField( max_length=64 ) event = models.OneToOneField( Event, primary_key=True ) class GCalPushChannel( models.Model ): """This table has either zero or one entry. Required to store if a channel already exists, when it expires, and how to stop (renew) the channel """ id = models.CharField( max_length=128, primary_key=True ) address = models.CharField( max_length=256 ) token = models.CharField( max_length=128 ) resource_id = models.CharField( max_length=128 ) expiration = models.IntegerField() def toGChannel( self ): return Channel( 'web_hook', self.id, self.token, self.address, self.expiration, resource_id = self.resource_id ) @staticmethod def fromGChannel( gChannel ): return GCalPushChannel( id = gChannel.id, address = gChannel.address, token = gChannel.token, expiration = gChannel.expiration, resource_id= gChannel.resource_id ) @staticmethod def createNew( callbackUrl, service, ttl = None ): gChannel = Channel('web_hook', str(uuid.uuid4()), 'blechreizGcal', callbackUrl, params= { 'ttl' : int(ttl) } ) response = service.events().watch( calendarId='primary', body= gChannel.body() ).execute() gChannel.update( response ) dbChannel = GCalPushChannel.fromGChannel( gChannel ) dbChannel.save() @staticmethod def stop( service, gChannel ): channelService = service.channels() channelService.stop( body = gChannel.body() ).execute() GCalPushChannel.fromGChannel( gChannel ).delete() def init( gcal_settings ): """Creates a API service object required by the following synchronization functions""" storage = Storage( gcal_settings['credentials_file'] ) credentials = storage.get() if credentials is None or credentials.invalid == True: logger.error("Unable to initialize Google Calendar coupling. Check your settings!") return None http = httplib2.Http() http = credentials.authorize( http ) return build( serviceName='calendar', version='v3', http=http, developerKey=gcal_settings['developerKey'] ) service = init( settings.GCAL_COUPLING ) def createAttendeesObj( event ): result = [] for u in User.objects.all(): if u.email.endswith( "@gmail.com") or u.email.endswith("@googlemail.com"): participation = EventParticipation.get_or_create( u, event ) status = "tentative" if participation.status == 'Yes': status = "accepted" if participation.status == 'No' : status = "declined" o = { 'id': u.email, 'email': u.email, 'displayName': u.username, 'comment': participation.comment, 'responseStatus': status, } result.append( o ) return result def buildGCalEvent( event, timezone="Europe/Berlin" ): if service is None: logger.error("createEvent: Google API connection not configured") return def createDateTimeObj( date, time ): if time is None: return { 'date': unicode(date), 'timeZone': timezone } else: return { 'dateTime': unicode(date) + 'T' + unicode(time) , 'timeZone': timezone } startDate = event.date endDate = event.end_date if endDate is None: endDate = startDate startTime = event.meeting_time if startTime is None: startTime = event.time if startTime is None: endTime = None else: endTime = datetime.time( 22, 30 ) return { 'summary': unicode(settings.GCAL_COUPLING['eventPrefix'] + event.title), 'description': unicode(event.desc), 'location': unicode(event.location), 'start': createDateTimeObj( startDate, startTime ), 'end' : createDateTimeObj( endDate, endTime ), 'extendedProperties': { 'private': { 'blechreizEvent': 'true', 'blechreizID': event.id, } }, 'attendees': createAttendeesObj( event ), } # ------------------------------------------------------------------------------- def getAllGCalEvents( fromNow=False, pageToken=None ): if fromNow: now = datetime.datetime.now() minTime = now.strftime("%Y-%m-%dT%H:%M:%S-00:00") else: minTime = '2000-01-01T00:00:00-00:00' events = service.events().list( calendarId='primary', singleEvents=True, maxResults=1000, orderBy='startTime', timeMin=minTime, timeMax='2100-01-01T00:00:00-00:00', pageToken=pageToken, privateExtendedProperty='blechreizEvent=true', ).execute() return events['items'] def createGCalEvent( event, timezone="Europe/Berlin" ): googleEvent = buildGCalEvent(event,timezone) return service.events().insert(calendarId='primary', body=googleEvent) def updateGCalEvent( event, timezone="Europe/Berlin"): googleEvent = buildGCalEvent(event,timezone) mapping = GCalMapping.objects.get( event=event ) gcalId = mapping.gcal_id return service.events().patch(calendarId='primary', eventId= gcalId, body=googleEvent) def deleteGCalEvent( event ): mapping = GCalMapping.objects.get( event=event ) gcalId = mapping.gcal_id return service.events().delete(calendarId='primary', eventId=gcalId) # ------------------------------------------------------------------------------- def deleteAllGCalEvents(): if service is None: logger.error("deleteAllGCalEvents: Google API connection not configured") return gcalIds = [ ev['id'] for ev in getAllGCalEvents() ] l = len(gcalIds) if l == 0: return l batch = BatchHttpRequest() for id in gcalIds: batch.add( service.events().delete(calendarId='primary', eventId=id) ) batch.execute() return l def syncGCalEvents(): if service is None: logger.error("syncGCalEvents: Google API connection not configured") return allEvents = getAllGCalEvents() eventsAtGoogle_djangoID = set() eventsAtGoogle_googleID = set() for gcalEv in allEvents: eventsAtGoogle_djangoID.add( int(gcalEv['extendedProperties']['private']['blechreizID'] ) ) eventsAtGoogle_googleID.add( gcalEv['id'] ) localEvents_djangoID = set( Event. objects.all().values_list('pk' , flat=True) ) localEvents_googleID = set( GCalMapping.objects.all().values_list('gcal_id', flat=True) ) eventsToCreate_djangoID = localEvents_djangoID - eventsAtGoogle_djangoID eventsToDelete_googleID = eventsAtGoogle_googleID - localEvents_googleID def onGcalEventCreated( request_id, response, exception ): """Callback function for created events""" if exception is not None: print ( "response " + str( response ) ) raise exception googleId = response['id'] djangoId = response['extendedProperties']['private']['blechreizID'] mapping = GCalMapping( gcal_id = googleId, event = Event.objects.get( pk=djangoId ) ) mapping.save() batch = BatchHttpRequest() batchIsEmpty = True for eventDjangoID in eventsToCreate_djangoID: batch.add( createGCalEvent( Event.objects.get( pk=eventDjangoID ) ), callback=onGcalEventCreated ) batchIsEmpty=False for eventGoogleID in eventsToDelete_googleID: batch.add( service.events().delete(calendarId='primary', eventId=eventGoogleID) ) batchIsEmpty=False if not batchIsEmpty: batch.execute() return len (eventsToCreate_djangoID), len(eventsToDelete_googleID ) def syncParticipationFromGoogleToLocal(): allEvents = getAllGCalEvents(fromNow=True) for e in allEvents: localId = e['extendedProperties']['private']['blechreizID'] localEvent = Event.objects.get( pk=localId ) for a in e['attendees']: user = User.objects.get( email= a['email'] ) part = EventParticipation.get_or_create( user, localEvent ) if 'comment' in a: part.comment = a['comment'] if a['responseStatus'] == 'needsAction' or a['responseStatus']=='tentative': part.status = '?' elif a['responseStatus'] == 'accepted': part.status = 'Yes' elif a['responseStatus'] == 'declined': part.status = 'No' else: logger.error("Unknown response status when mapping gcal event: " + a['responseStatus'] ) part.save() def checkGCalSubscription(): global service callbackUrl = settings.GCAL_COUPLING['push_url'] #timeToLive = 14*24*3600 # how long the channel should be active #renewBeforeExpiry = 2*24*3600 # duration before expiry when channel is renewed timeToLive = 60*5 renewBeforeExpiry = 60*3 # Test if a channel already exists for this callbackURL try: dbChannel = GCalPushChannel.objects.get( address=callbackUrl ) gChannel = dbChannel.toGChannel() # if expiration time between 0 and two days: stop and create new channel curTime = int( time.time() * 1000) if gChannel.expiration > curTime: # not yet expired if curTime + renewBeforeExpiry*1000 > gChannel.expiration: #will expire in less than "renewBeforeExpiry" print ( "Renewing Google Calendar Subscription: " + callbackUrl ) GCalPushChannel.stop( service, gChannel ) GCalPushChannel.createNew( callbackUrl, service, timeToLive ) else: print ("Channel active until %d " % ( gChannel.expiration, ) ) else: print( "Google calendar subscription had expired - getting new subscription" ) syncParticipationFromGoogleToLocal() GCalPushChannel.createNew( callbackUrl, service, timeToLive ) except GCalPushChannel.DoesNotExist: # create new channel and save it in database print ( "No CGalCallback Channel exists yet for: " + callbackUrl ) syncParticipationFromGoogleToLocal() GCalPushChannel.createNew( callbackUrl, service, timeToLive ) def stopAllGCalSubscriptions(): for dbChannel in GCalPushChannel.objects.all(): print("Stopping %s expiry at %d " % ( dbChannel.id, dbChannel.expiration ) ) GCalPushChannel.stop( service, dbChannel.toGChannel() )