diff --git a/api/integrations/google/get-google-credentials b/api/integrations/google/get-google-credentials
new file mode 100644
index 0000000000..e91938859e
--- /dev/null
+++ b/api/integrations/google/get-google-credentials
@@ -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()
diff --git a/api/integrations/google/google-calendar b/api/integrations/google/google-calendar
index 1064c3db6e..014b7e48e6 100755
--- a/api/integrations/google/google-calendar
+++ b/api/integrations/google/google-calendar
@@ -1,92 +1,131 @@
#!/usr/bin/env python
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 time
-import datetime
-import optparse
-from six.moves import urllib
-import itertools
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 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"""
%prog \
--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:
- - 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"
+ Before running this integration make sure you run the get-google-credentials file to give Zulip
+ access to certain aspects of your Google Account.
- Run this on your personal machine. Your API key and calendar URL are revealed to local
- users through the command line.
+ 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: 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',
dest='interval',
- default=10,
+ default=30,
type=int,
action='store',
- help='Minutes before event for reminder [default: 10]',
+ 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 and options.calendar):
- parser.error('You must specify --user and --calendar')
+if not (options.zulip_email):
+ parser.error('You must specify --user')
-try:
- from gdata.calendar.client import CalendarClient
-except ImportError:
- parser.error('Install python-gdata')
+zulip_client = zulip.init_from_options(options)
-def get_calendar_url():
- # type: () -> str
- 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', ''))
+def get_credentials():
+ # type: () -> client.Credentials
+ """Gets valid user credentials from storage.
-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():
# 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:
- start = event.when[0].start.split('.')[0]
+ 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)
- 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():
# type: () -> Optional[None]
@@ -96,14 +135,17 @@ def send_reminders():
keys = set()
now = datetime.datetime.now()
- for uid, start, title in events:
+ for id, start, summary in events:
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
# repeating events.
- key = (uid, start)
+ key = (id, start)
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)
messages.append(line)
keys.add(key)
@@ -116,12 +158,13 @@ def send_reminders():
else:
message = 'Reminder:\n\n' + '\n'.join('* ' + m for m in messages)
- client.send_message(dict(
+ zulip_client.send_message(dict(
type = 'private',
- to = options.user,
+ to = options.zulip_email,
+ sender = options.zulip_email,
content = message))
- sent |= keys
+ sent.update(keys)
# Loop forever
for i in itertools.count():
@@ -132,5 +175,5 @@ for i in itertools.count():
events = list(get_events())
send_reminders()
except:
- traceback.print_exc()
+ logging.exception("Couldn't download Google calendar and/or couldn't post to Zulip.")
time.sleep(60)
diff --git a/static/images/integrations/google/calendar/001.png b/static/images/integrations/google/calendar/001.png
new file mode 100644
index 0000000000..18953f0986
Binary files /dev/null and b/static/images/integrations/google/calendar/001.png differ
diff --git a/static/images/integrations/google/calendar/002.png b/static/images/integrations/google/calendar/002.png
new file mode 100644
index 0000000000..420a40fcce
Binary files /dev/null and b/static/images/integrations/google/calendar/002.png differ
diff --git a/static/images/integrations/google/calendar/003.png b/static/images/integrations/google/calendar/003.png
new file mode 100644
index 0000000000..abf2760e94
Binary files /dev/null and b/static/images/integrations/google/calendar/003.png differ
diff --git a/static/images/integrations/logos/google-calendar.png b/static/images/integrations/logos/google-calendar.png
new file mode 100644
index 0000000000..dc82095046
Binary files /dev/null and b/static/images/integrations/logos/google-calendar.png differ
diff --git a/templates/zerver/integrations.html b/templates/zerver/integrations.html
index ff9aa6b429..c8cb2c2c8a 100644
--- a/templates/zerver/integrations.html
+++ b/templates/zerver/integrations.html
@@ -813,7 +813,6 @@
-
First, download and install our Python @@ -1009,6 +1008,95 @@
+ Get Google Calendar reminders in Zulip! This is a great way to see your reminders directly in + your Zulip feed. +
+ ++ First download and install our Python Bindings and example scripts. + 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. +
+ +
+ Next, follow the instructions for Step 1 at this link to get a client_secret
+ file. Save this file to your /~ directory instead of your working directory.
+
+ Next, install the latest Google API Client for Python by following the instructions on + the + Google Website. +
+ ++ Then go to your Zulip Settings by clicking on the cog in the top right corner, + and then clicking on Settings. +
+ +
+ Click on the tab that's labeled Your Bots and click on Show/change your API
+ key. Enter your password if prompted, and download the .zuliprc file. Save
+ this file to your ~/ folder.
+

+ Run the get-google-credentials with this command:
+
python /usr/local/share/zulip/integrations/google/get-google-credentials+ 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. + + +
+ Now, all that's left to do is to run the gcal-bot script, in the same
+ directory as the get-google-credentials script, with the necessary paramaters:
+
python /usr/local/share/zulip/integrations/google/gcal-bot --user foo@zulip.com+ + +
+ The --user flag specifies the user to send the reminder to.
There are
+ two optional flags that you can specify when running this script:
+
--calendar: 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
+ Calendar settings in the drop down, and look for the Calendar Address
+ section. Copy the Calendar ID from the right side of the page and use that as the
+ value for this flag.
+
+ 
--interval: 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.
+ + Don't close the terminal window with the bot running. You will only get reminders if the + bot is still running.
+ +
+ Congratulations! You're done!
You will get a Zulip private message, whenever you
+ have a calendar event scheduled, that looks like this:
+
+
Learn how Zulip integrations work with this simple Hello World example!
diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py index 76f6204821..9f893ebda3 100644 --- a/zerver/lib/integrations.py +++ b/zerver/lib/integrations.py @@ -163,6 +163,7 @@ INTEGRATIONS = { 'codebase': Integration('codebase', 'codebase'), 'email': Integration('email', 'email'), 'git': Integration('git', 'git'), + 'google-calendar': Integration('google-calendar', 'google-calendar', display_name='Google Calendar'), 'hubot': Integration('hubot', 'hubot'), 'jenkins': Integration('jenkins', 'jenkins', secondary_line_text='(or Hudson)'), 'jira-plugin': Integration(