2014-04-19 20:36:01 +02:00
|
|
|
from apiclient.channel import new_webhook_channel, Channel
|
2014-03-08 22:36:25 +01:00
|
|
|
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
|
2014-04-19 20:36:01 +02:00
|
|
|
import time
|
|
|
|
import uuid
|
2014-03-08 22:36:25 +01:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GCalMapping( models.Model ):
|
|
|
|
gcal_id = models.CharField( max_length=64 )
|
|
|
|
event = models.OneToOneField( Event, primary_key=True )
|
|
|
|
|
|
|
|
|
2014-04-19 20:36:01 +02:00
|
|
|
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()
|
|
|
|
|
|
|
|
|
2014-03-08 22:36:25 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'] )
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-04-19 20:36:01 +02:00
|
|
|
service = init( settings.GCAL_COUPLING )
|
2014-03-08 22:36:25 +01:00
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2014-03-09 18:13:30 +01:00
|
|
|
def buildGCalEvent( event, timezone="Europe/Berlin" ):
|
2014-03-08 22:36:25 +01:00
|
|
|
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 )
|
|
|
|
|
2014-03-09 18:13:30 +01:00
|
|
|
return {
|
2014-03-08 22:36:25 +01:00
|
|
|
'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 ),
|
|
|
|
}
|
|
|
|
|
2014-03-09 18:13:30 +01:00
|
|
|
# -------------------------------------------------------------------------------
|
2014-03-08 22:36:25 +01:00
|
|
|
|
2014-04-18 13:43:02 +02:00
|
|
|
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'
|
|
|
|
|
2014-03-08 22:36:25 +01:00
|
|
|
events = service.events().list(
|
|
|
|
calendarId='primary',
|
|
|
|
singleEvents=True,
|
|
|
|
maxResults=1000,
|
|
|
|
orderBy='startTime',
|
2014-04-18 13:43:02 +02:00
|
|
|
timeMin=minTime,
|
2014-03-08 22:36:25 +01:00
|
|
|
timeMax='2100-01-01T00:00:00-00:00',
|
|
|
|
pageToken=pageToken,
|
|
|
|
privateExtendedProperty='blechreizEvent=true',
|
|
|
|
).execute()
|
|
|
|
return events['items']
|
|
|
|
|
|
|
|
|
2014-03-09 18:13:30 +01:00
|
|
|
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
|
|
|
|
|
2014-03-08 22:36:25 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def syncGCalEvents():
|
|
|
|
if service is None:
|
|
|
|
logger.error("syncGCalEvents: Google API connection not configured")
|
|
|
|
return
|
|
|
|
|
2014-03-09 18:13:30 +01:00
|
|
|
allEvents = getAllGCalEvents()
|
2014-03-08 22:36:25 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2014-03-09 18:13:30 +01:00
|
|
|
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()
|
|
|
|
|
|
|
|
|
2014-03-08 22:36:25 +01:00
|
|
|
batch = BatchHttpRequest()
|
|
|
|
|
|
|
|
batchIsEmpty = True
|
|
|
|
for eventDjangoID in eventsToCreate_djangoID:
|
2014-03-09 18:13:30 +01:00
|
|
|
batch.add( createGCalEvent( Event.objects.get( pk=eventDjangoID ) ), callback=onGcalEventCreated )
|
2014-03-08 22:36:25 +01:00
|
|
|
batchIsEmpty=False
|
|
|
|
|
|
|
|
for eventGoogleID in eventsToDelete_googleID:
|
|
|
|
batch.add( service.events().delete(calendarId='primary', eventId=eventGoogleID) )
|
|
|
|
batchIsEmpty=False
|
|
|
|
|
|
|
|
if not batchIsEmpty:
|
|
|
|
batch.execute()
|
|
|
|
|
2014-03-09 18:13:30 +01:00
|
|
|
return len (eventsToCreate_djangoID), len(eventsToDelete_googleID )
|
2014-03-08 22:36:25 +01:00
|
|
|
|
|
|
|
|
2014-04-18 13:43:02 +02:00
|
|
|
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'] )
|
|
|
|
|
2014-04-19 20:36:01 +02:00
|
|
|
part.save()
|
|
|
|
|
|
|
|
|
|
|
|
def checkGCalSubscription():
|
|
|
|
global service
|
|
|
|
callbackUrl = settings.GCAL_COUPLING['push_url']
|
|
|
|
|
2014-04-21 09:21:59 +02:00
|
|
|
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
|
2014-04-19 20:36:01 +02:00
|
|
|
|
|
|
|
# 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 ) )
|
2014-04-21 09:21:59 +02:00
|
|
|
GCalPushChannel.stop( service, dbChannel.toGChannel() )
|