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 5a51f5f9d5
commit bf71ad162c
8 changed files with 248 additions and 60 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,92 +1,131 @@
#!/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
from oauth2client import client, tools
from oauth2client.file import Storage
try:
from googleapiclient import discovery
except ImportError:
logging.exception('Install google-api-python-client')
sys.path.append(os.path.join(os.path.dirname(__file__), '../../'))
import zulip
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
CLIENT_SECRET_FILE = 'client_secret.json'
APPLICATION_NAME = 'Zulip'
HOME_DIR = os.path.expanduser('~')
# Our cached view of the calendar, updated periodically.
events = [] # type: List[Tuple[int, datetime.datetime, str]]
# Unique keys for events we've already sent, so we don't remind twice.
sent = set() # type: Set[Tuple[int, datetime.datetime]]
sys.path.append(os.path.dirname(__file__))
parser = optparse.OptionParser(r""" parser = optparse.OptionParser(r"""
%prog \ %prog \
--user foo@zulip.com \ --user foo@zulip.com \
--calendar http://www.google.com/calendar/feeds/foo%40zulip.com/private-fedcba9876543210fedcba9876543210/basic --calendar calendarID@example.calendar.google.com
Send yourself reminders on Zulip of Google Calendar events. This integration can be used to send yourself reminders, on Zulip, of Google Calendar Events.
To get the calendar URL: Before running this integration make sure you run the get-google-credentials file to give Zulip
- Load Google Calendar in a web browser access to certain aspects of your Google Account.
- 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 This integration should be run on your local machine. Your API key and other information are
users through the command line. revealed to local users through the command line.
Depends on: python-gdata Depends on: google-api-python-client
""") """)
parser.add_option('--calendar',
dest='calendar',
action='store',
help='Google Calendar XML "Private Address"',
metavar='URL')
parser.add_option('--interval', parser.add_option('--interval',
dest='interval', dest='interval',
default=10, default=30,
type=int, type=int,
action='store', action='store',
help='Minutes before event for reminder [default: 10]', help='Minutes before event for reminder [default: 30]',
metavar='MINUTES') 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)) parser.add_option_group(zulip.generate_option_group(parser))
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
if not (options.zulip_email and options.calendar): if not (options.zulip_email):
parser.error('You must specify --user and --calendar') parser.error('You must specify --user')
try: zulip_client = zulip.init_from_options(options)
from gdata.calendar.client import CalendarClient
except ImportError:
parser.error('Install python-gdata')
def get_calendar_url(): def get_credentials():
# type: () -> str # type: () -> client.Credentials
parts = urllib.parse.urlparse(options.calendar) """Gets valid user credentials from storage.
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() 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.")
client = zulip.init_from_options(options)
def get_events(): def get_events():
# type: () -> Iterable[Tuple[int, datetime.datetime, str]] # type: () -> Iterable[Tuple[int, datetime.datetime, str]]
feed = CalendarClient().GetCalendarEventFeed(uri=calendar_url) credentials = get_credentials()
creds = credentials.authorize(httplib2.Http())
service = discovery.build('calendar', 'v3', http=creds)
for event in feed.entry: now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
start = event.when[0].start.split('.')[0] 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 # All-day events can have only a date
fmt = '%Y-%m-%dT%H:%M:%S' if 'T' in start else '%Y-%m-%d' fmt = '%Y-%m-%dT%H:%M:%S' if 'T' in start else '%Y-%m-%d'
start = datetime.datetime.strptime(start, fmt) start = datetime.datetime.strptime(start, fmt)
yield (event.uid.value, start, event.title.text) try:
yield (event["id"], start, event["summary"])
except KeyError:
yield (event["id"], start, "(No Title)")
# Our cached view of the calendar, updated periodically.
events = [] # type: List[Tuple[int, datetime.datetime, str]]
# Unique keys for events we've already sent, so we don't remind twice.
sent = set() # type: Set[Tuple[int, datetime.datetime]]
def send_reminders(): def send_reminders():
# type: () -> Optional[None] # type: () -> Optional[None]
@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -813,7 +813,6 @@
<img class="screenshot" src="/static/images/integrations/freshdesk/010.png" /> <img class="screenshot" src="/static/images/integrations/freshdesk/010.png" />
</div> </div>
<div id="git" class="integration-instructions"> <div id="git" class="integration-instructions">
<p>First, download and install our <a href="/api">Python <p>First, download and install our <a href="/api">Python
@@ -1009,6 +1008,95 @@
</div> </div>
<div id="google-calendar" class="integration-instructions">
<p>
Get Google Calendar reminders in Zulip! This is a great way to see your reminders directly in
your Zulip feed.
</p>
<p>
First download and install our <a href="/api">Python Bindings and example scripts</a>.
This bot should be set up on a trusted machine, because your API key is visible to local users
through the command line or config file.
</p>
<p>
Next, follow the instructions for <b>Step 1</b> at <a href="https://developers.google.com/
google-apps/calendar/quickstart/python">this link</a> to get a <code>client_secret</code>
file. Save this file to your <code>/~</code> directory instead of your working directory.
</p>
<p>
Next, install the latest Google API Client for Python by following the instructions on
the <a href="https://developers.google.com/api-client-library/python/start/installation">
Google Website</a>.
</p>
<p>
Then go to your <b>Zulip Settings</b> by clicking on the cog in the top right corner,
and then clicking on <b>Settings</b>.
</p>
<p>
Click on the tab that's labeled <b>Your Bots</b> and click on <b>Show/change your API
key</b>. Enter your password if prompted, and download the <code>.zuliprc</code> file. Save
this file to your <code>~/</code> folder.
</p>
<p><img class="screenshot" src="/static/images/integrations/google/calendar/001.png" /></p>
<p>
Run the <code>get-google-credentials</code> with this command:
<pre>python /usr/local/share/zulip/integrations/google/get-google-credentials</pre>
It should open up a browser and ask you for certain permissions. Give Zulip access, and move
on to the next step. If it doesn't open a browser, follow the instructions in the terminal
window.
</p>
<p>
Now, all that's left to do is to run the <code>gcal-bot</code> script, in the same
directory as the <code>get-google-credentials</code> script, with the necessary paramaters:
<pre>python /usr/local/share/zulip/integrations/google/gcal-bot --user foo@zulip.com</pre>
</p>
<p>
The <code>--user</code> flag specifies the user to send the reminder to. <br />There are
two optional flags that you can specify when running this script:
</p>
<ol>
<li>
<code>--calendar</code>: This flag specifies the calendar to watch from the user's
Google Account. By default, this flag is set to a user's primary or default calendar.
To specify a calendar, you need the calendar ID which can be obtained by going to Google
Calendar and clicking on the wedge next to the calendar's name. Click on settings in
<b>Calendar settings</b> in the drop down, and look for the <b>Calendar Address</b>
section. Copy the <b>Calendar ID</b> from the right side of the page and use that as the
value for this flag.
<p><img class="screenshot" src="/static/images/integrations/google/calendar/002.png"/></p>
</li>
<li>
<code>--interval</code>: This flag specifies the interval of time - in minutes - between
receiving the reminder, and the actual event. For example, an interval of 30 minutes
would mean that you would recieve a reminder for an event 30 minutes before it is
scheduled to occur.
</li>
</ol>
<p>
Don't close the terminal window with the bot running. You will only get reminders if the
bot is still running.</p>
<p>
<b>Congratulations! You're done!</b><br />You will get a Zulip private message, whenever you
have a calendar event scheduled, that looks like this:
<img class="screenshot" src="/static/images/integrations/google/calendar/003.png" />
</p>
</div>
<div id="helloworld" class="integration-instructions"> <div id="helloworld" class="integration-instructions">
<p>Learn how Zulip integrations work with this simple Hello World example!</p> <p>Learn how Zulip integrations work with this simple Hello World example!</p>

View File

@@ -163,6 +163,7 @@ INTEGRATIONS = {
'codebase': Integration('codebase', 'codebase'), 'codebase': Integration('codebase', 'codebase'),
'email': Integration('email', 'email'), 'email': Integration('email', 'email'),
'git': Integration('git', 'git'), 'git': Integration('git', 'git'),
'google-calendar': Integration('google-calendar', 'google-calendar', display_name='Google Calendar'),
'hubot': Integration('hubot', 'hubot'), 'hubot': Integration('hubot', 'hubot'),
'jenkins': Integration('jenkins', 'jenkins', secondary_line_text='(or Hudson)'), 'jenkins': Integration('jenkins', 'jenkins', secondary_line_text='(or Hudson)'),
'jira-plugin': Integration( 'jira-plugin': Integration(