python-zulip-api/integrations/google/google-calendar

201 lines
6.5 KiB
Python
Executable file

#!/usr/bin/env python
#
# This script depends on python-dateutil and python-pytz for properly handling
# times and time zones of calendar events.
from __future__ import print_function
import datetime
import dateutil.parser
import httplib2
import itertools
import logging
import optparse
import os
import pytz
from six.moves import urllib
import sys
import time
import traceback
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 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 populate_events():
# type: () -> Optional[None]
global events
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()
events = []
for event in feed["items"]:
try:
start = dateutil.parser.parse(event["start"]["dateTime"])
# According to the API documentation, a time zone offset is required
# for start.dateTime unless a time zone is explicitly specified in
# start.timeZone.
if start.tzinfo is None:
event_timezone = pytz.timezone(event["start"]["timeZone"])
# pytz timezones include an extra localize method that's not part
# of the tzinfo base class.
start = event_timezone.localize(start) # type: ignore
except KeyError:
# All-day events can have only a date.
start_naive = dateutil.parser.parse(event["start"]["date"])
# All-day events don't have a time zone offset; instead, we use the
# time zone of the calendar.
calendar_timezone = pytz.timezone(feed["timeZone"])
# pytz timezones include an extra localize method that's not part
# of the tzinfo base class.
start = calendar_timezone.localize(start_naive) # type: ignore
try:
events.append((event["id"], start, event["summary"]))
except KeyError:
events.append((event["id"], start, "(No Title)"))
def send_reminders():
# type: () -> Optional[None]
global sent
messages = []
keys = set()
now = datetime.datetime.now(tz=pytz.utc)
for id, start, summary in events:
dt = start - now
if dt.days == 0 and dt.seconds < 60 * options.interval:
# The unique key includes the start time, because of
# repeating events.
key = (id, start)
if key not in sent:
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)
if not messages:
return
if len(messages) == 1:
message = 'Reminder: ' + messages[0]
else:
message = 'Reminder:\n\n' + '\n'.join('* ' + m for m in messages)
zulip_client.send_message(dict(
type = 'private',
to = options.zulip_email,
sender = options.zulip_email,
content = message))
sent.update(keys)
# Loop forever
for i in itertools.count():
try:
# We check reminders every minute, but only
# download the calendar every 10 minutes.
if not i % 10:
populate_events()
send_reminders()
except:
logging.exception("Couldn't download Google calendar and/or couldn't post to Zulip.")
time.sleep(60)