Update Google Calendar Integration.

Update integration to use the latest Google API client.
Move Google Account authorization code to a separate file.
Move relevant files from 'bots/' to 'api/integrations/google/'.
Add documentation for integration.
This commit is contained in:
Vamshi Balanaga 2016-12-15 20:38:44 -05:00 committed by Tim Abbott
parent b352dc85f0
commit 88bdcd61b8
2 changed files with 158 additions and 59 deletions

View file

@ -0,0 +1,56 @@
#!/usr/bin/env python
from __future__ import print_function
import datetime
import httplib2
import os
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage
try:
import argparse
flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()
except ImportError:
flags = None
# If modifying these scopes, delete your previously saved credentials
# at zulip/bots/gcal/
# NOTE: When adding more scopes, add them after the previous one in the same field, with a space
# seperating them.
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
# This file contains the information that google uses to figure out which application is requesting
# this client's data.
CLIENT_SECRET_FILE = 'client_secret.json'
APPLICATION_NAME = 'Zulip Calendar Bot'
HOME_DIR = os.path.expanduser('~')
def get_credentials():
# type: () -> client.Credentials
"""Gets valid user credentials from storage.
If nothing has been stored, or if the stored credentials are invalid,
the OAuth2 flow is completed to obtain the new credentials.
Returns:
Credentials, the obtained credential.
"""
credential_path = os.path.join(HOME_DIR,
'google-credentials.json')
store = Storage(credential_path)
credentials = store.get()
if not credentials or credentials.invalid:
flow = client.flow_from_clientsecrets(os.path.join(HOME_DIR, CLIENT_SECRET_FILE), SCOPES)
flow.user_agent = APPLICATION_NAME
if flags:
# This attempts to open an authorization page in the default web browser, and asks the user
# to grant the bot access to their data. If the user grants permission, the run_flow()
# function returns new credentials.
credentials = tools.run_flow(flow, store, flags)
else: # Needed only for compatibility with Python 2.6
credentials = tools.run(flow, store)
print('Storing credentials to ' + credential_path)
get_credentials()

View file

@ -1,86 +1,31 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import print_function from __future__ import print_function
import datetime
import httplib2
import itertools
import logging
import optparse
import os
from six.moves import urllib
import sys import sys
import time import time
import datetime
import optparse
from six.moves import urllib
import itertools
import traceback import traceback
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../api'))
import zulip
from typing import List, Set, Tuple, Iterable, Optional from typing import List, Set, Tuple, Iterable, Optional
parser = optparse.OptionParser(r""" from oauth2client import client, tools
from oauth2client.file import Storage
%prog \
--user foo@zulip.com \
--calendar http://www.google.com/calendar/feeds/foo%40zulip.com/private-fedcba9876543210fedcba9876543210/basic
Send yourself reminders on Zulip of Google Calendar events.
To get the calendar URL:
- Load Google Calendar in a web browser
- Find your calendar in the "My calendars" list on the left
- Click the down-wedge icon that appears on mouseover, and select "Calendar settings"
- Copy the link address for the "XML" button under "Private Address"
Run this on your personal machine. Your API key and calendar URL are revealed to local
users through the command line.
Depends on: python-gdata
""")
parser.add_option('--calendar',
dest='calendar',
action='store',
help='Google Calendar XML "Private Address"',
metavar='URL')
parser.add_option('--interval',
dest='interval',
default=10,
type=int,
action='store',
help='Minutes before event for reminder [default: 10]',
metavar='MINUTES')
parser.add_option_group(zulip.generate_option_group(parser))
(options, args) = parser.parse_args()
if not (options.zulip_email and options.calendar):
parser.error('You must specify --user and --calendar')
try: try:
from gdata.calendar.client import CalendarClient from googleapiclient import discovery
except ImportError: except ImportError:
parser.error('Install python-gdata') logging.exception('Install google-api-python-client')
def get_calendar_url(): sys.path.append(os.path.join(os.path.dirname(__file__), '../../'))
# type: () -> str import zulip
parts = urllib.parse.urlparse(options.calendar)
pat = os.path.split(parts.path)
if pat[1] != 'basic':
parser.error('The --calendar URL should be the XML "Private Address" ' +
'from your calendar settings')
return urllib.parse.urlunparse((parts.scheme, parts.netloc, pat[0] + '/full',
'', 'futureevents=true&orderby=startdate', ''))
calendar_url = get_calendar_url() SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
CLIENT_SECRET_FILE = 'client_secret.json'
client = zulip.init_from_options(options) APPLICATION_NAME = 'Zulip'
HOME_DIR = os.path.expanduser('~')
def get_events():
# type: () -> Iterable[Tuple[int, datetime.datetime, str]]
feed = CalendarClient().GetCalendarEventFeed(uri=calendar_url)
for event in feed.entry:
start = event.when[0].start.split('.')[0]
# All-day events can have only a date
fmt = '%Y-%m-%dT%H:%M:%S' if 'T' in start else '%Y-%m-%d'
start = datetime.datetime.strptime(start, fmt)
yield (event.uid.value, start, event.title.text)
# Our cached view of the calendar, updated periodically. # Our cached view of the calendar, updated periodically.
events = [] # type: List[Tuple[int, datetime.datetime, str]] events = [] # type: List[Tuple[int, datetime.datetime, str]]
@ -88,6 +33,100 @@ events = [] # type: List[Tuple[int, datetime.datetime, str]]
# Unique keys for events we've already sent, so we don't remind twice. # Unique keys for events we've already sent, so we don't remind twice.
sent = set() # type: Set[Tuple[int, datetime.datetime]] sent = set() # type: Set[Tuple[int, datetime.datetime]]
sys.path.append(os.path.dirname(__file__))
parser = optparse.OptionParser(r"""
%prog \
--user foo@zulip.com \
--calendar calendarID@example.calendar.google.com
This integration can be used to send yourself reminders, on Zulip, of Google Calendar Events.
Before running this integration make sure you run the get-google-credentials file to give Zulip
access to certain aspects of your Google Account.
This integration should be run on your local machine. Your API key and other information are
revealed to local users through the command line.
Depends on: google-api-python-client
""")
parser.add_option('--interval',
dest='interval',
default=30,
type=int,
action='store',
help='Minutes before event for reminder [default: 30]',
metavar='MINUTES')
parser.add_option('--calendar',
dest = 'calendarID',
default = 'primary',
type = str,
action = 'store',
help = 'Calendar ID for the calendar you want to receive reminders from.')
parser.add_option_group(zulip.generate_option_group(parser))
(options, args) = parser.parse_args()
if not (options.zulip_email):
parser.error('You must specify --user')
zulip_client = zulip.init_from_options(options)
def get_credentials():
# type: () -> client.Credentials
"""Gets valid user credentials from storage.
If nothing has been stored, or if the stored credentials are invalid,
an exception is thrown and the user is informed to run the script in this directory to get
credentials.
Returns:
Credentials, the obtained credential.
"""
try:
credential_path = os.path.join(HOME_DIR,
'google-credentials.json')
store = Storage(credential_path)
credentials = store.get()
return credentials
except client.Error:
logging.exception('Error while trying to open the `google-credentials.json` file.')
except IOError:
logging.error("Run the get-google-credentials script from this directory first.")
def get_events():
# type: () -> Iterable[Tuple[int, datetime.datetime, str]]
credentials = get_credentials()
creds = credentials.authorize(httplib2.Http())
service = discovery.build('calendar', 'v3', http=creds)
now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
feed = service.events().list(calendarId=options.calendarID, timeMin=now, maxResults=5,
singleEvents=True, orderBy='startTime').execute()
for event in feed["items"]:
try:
start = event["start"]["dateTime"]
except KeyError:
start = event["start"]["date"]
start = start[:19]
# All-day events can have only a date
fmt = '%Y-%m-%dT%H:%M:%S' if 'T' in start else '%Y-%m-%d'
start = datetime.datetime.strptime(start, fmt)
try:
yield (event["id"], start, event["summary"])
except KeyError:
yield (event["id"], start, "(No Title)")
def send_reminders(): def send_reminders():
# type: () -> Optional[None] # type: () -> Optional[None]
global sent global sent
@ -96,14 +135,17 @@ def send_reminders():
keys = set() keys = set()
now = datetime.datetime.now() now = datetime.datetime.now()
for uid, start, title in events: for id, start, summary in events:
dt = start - now dt = start - now
if dt.days == 0 and dt.seconds < 60 * options.interval: if dt.days == 0 and dt.seconds < 60 * options.interval:
# The unique key includes the start time, because of # The unique key includes the start time, because of
# repeating events. # repeating events.
key = (uid, start) key = (id, start)
if key not in sent: if key not in sent:
line = '%s starts at %s' % (title, start.strftime('%H:%M')) if start.hour == 0 and start.minute == 0:
line = '%s is today.' % (summary)
else:
line = '%s starts at %s' % (summary, start.strftime('%H:%M'))
print('Sending reminder:', line) print('Sending reminder:', line)
messages.append(line) messages.append(line)
keys.add(key) keys.add(key)
@ -116,12 +158,13 @@ def send_reminders():
else: else:
message = 'Reminder:\n\n' + '\n'.join('* ' + m for m in messages) message = 'Reminder:\n\n' + '\n'.join('* ' + m for m in messages)
client.send_message(dict( zulip_client.send_message(dict(
type = 'private', type = 'private',
to = options.user, to = options.zulip_email,
sender = options.zulip_email,
content = message)) content = message))
sent |= keys sent.update(keys)
# Loop forever # Loop forever
for i in itertools.count(): for i in itertools.count():
@ -132,5 +175,5 @@ for i in itertools.count():
events = list(get_events()) events = list(get_events())
send_reminders() send_reminders()
except: except:
traceback.print_exc() logging.exception("Couldn't download Google calendar and/or couldn't post to Zulip.")
time.sleep(60) time.sleep(60)