mirror of
https://github.com/zulip/zulip.git
synced 2025-11-09 00:18:12 +00:00
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:
committed by
Tim Abbott
parent
5a51f5f9d5
commit
bf71ad162c
56
api/integrations/google/get-google-credentials
Normal file
56
api/integrations/google/get-google-credentials
Normal 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()
|
||||
@@ -1,86 +1,31 @@
|
||||
#!/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
|
||||
|
||||
parser = optparse.OptionParser(r"""
|
||||
|
||||
%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')
|
||||
|
||||
from oauth2client import client, tools
|
||||
from oauth2client.file import Storage
|
||||
try:
|
||||
from gdata.calendar.client import CalendarClient
|
||||
from googleapiclient import discovery
|
||||
except ImportError:
|
||||
parser.error('Install python-gdata')
|
||||
logging.exception('Install google-api-python-client')
|
||||
|
||||
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', ''))
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../'))
|
||||
import zulip
|
||||
|
||||
calendar_url = get_calendar_url()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
||||
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)
|
||||
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]]
|
||||
@@ -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.
|
||||
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():
|
||||
# type: () -> Optional[None]
|
||||
global sent
|
||||
@@ -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)
|
||||
|
||||
BIN
static/images/integrations/google/calendar/001.png
Normal file
BIN
static/images/integrations/google/calendar/001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
static/images/integrations/google/calendar/002.png
Normal file
BIN
static/images/integrations/google/calendar/002.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
static/images/integrations/google/calendar/003.png
Normal file
BIN
static/images/integrations/google/calendar/003.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
BIN
static/images/integrations/logos/google-calendar.png
Normal file
BIN
static/images/integrations/logos/google-calendar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -813,7 +813,6 @@
|
||||
<img class="screenshot" src="/static/images/integrations/freshdesk/010.png" />
|
||||
</div>
|
||||
|
||||
|
||||
<div id="git" class="integration-instructions">
|
||||
|
||||
<p>First, download and install our <a href="/api">Python
|
||||
@@ -1009,6 +1008,95 @@
|
||||
|
||||
</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">
|
||||
|
||||
<p>Learn how Zulip integrations work with this simple Hello World example!</p>
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user