2020-04-02 09:59:28 -04:00
|
|
|
#!/usr/bin/env python3
|
2017-02-25 03:16:52 -05:00
|
|
|
#
|
|
|
|
# This script depends on python-dateutil and python-pytz for properly handling
|
|
|
|
# times and time zones of calendar events.
|
2021-05-28 05:00:04 -04:00
|
|
|
import argparse
|
2012-10-21 16:18:51 -04:00
|
|
|
import datetime
|
2016-12-15 20:38:44 -05:00
|
|
|
import itertools
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import time
|
2020-04-18 19:00:35 -04:00
|
|
|
from typing import List, Optional, Set, Tuple
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2021-05-28 05:00:04 -04:00
|
|
|
import dateutil.parser
|
|
|
|
import httplib2
|
|
|
|
import pytz
|
2020-04-18 19:00:35 -04:00
|
|
|
from oauth2client import client
|
2016-12-15 20:38:44 -05:00
|
|
|
from oauth2client.file import Storage
|
2021-05-28 05:00:04 -04:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
try:
|
|
|
|
from googleapiclient import discovery
|
|
|
|
except ImportError:
|
2021-05-28 05:05:11 -04:00
|
|
|
logging.exception("Install google-api-python-client")
|
2016-12-15 20:38:44 -05:00
|
|
|
|
2021-05-28 05:05:11 -04:00
|
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
|
2013-08-07 11:51:03 -04:00
|
|
|
import zulip
|
2016-12-15 20:38:44 -05:00
|
|
|
|
2021-05-28 05:05:11 -04:00
|
|
|
SCOPES = "https://www.googleapis.com/auth/calendar.readonly"
|
|
|
|
CLIENT_SECRET_FILE = "client_secret.json"
|
|
|
|
APPLICATION_NAME = "Zulip"
|
|
|
|
HOME_DIR = os.path.expanduser("~")
|
2016-12-15 20:38:44 -05:00
|
|
|
|
|
|
|
# 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__))
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2021-05-28 05:03:46 -04:00
|
|
|
parser = zulip.add_default_arguments(
|
|
|
|
argparse.ArgumentParser(
|
|
|
|
r"""
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2017-08-25 05:03:06 -04:00
|
|
|
google-calendar --calendar calendarID@example.calendar.google.com
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
This integration can be used to send yourself reminders, on Zulip, of Google Calendar Events.
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2017-08-25 05:03:06 -04:00
|
|
|
Specify your Zulip API credentials and server in a ~/.zuliprc file or using the options.
|
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
Before running this integration make sure you run the get-google-credentials file to give Zulip
|
|
|
|
access to certain aspects of your Google Account.
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
This integration should be run on your local machine. Your API key and other information are
|
|
|
|
revealed to local users through the command line.
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
Depends on: google-api-python-client
|
2021-05-28 05:03:46 -04:00
|
|
|
"""
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
parser.add_argument(
|
2021-05-28 05:05:11 -04:00
|
|
|
"--interval",
|
|
|
|
dest="interval",
|
2021-05-28 05:03:46 -04:00
|
|
|
default=30,
|
|
|
|
type=int,
|
2021-05-28 05:05:11 -04:00
|
|
|
action="store",
|
|
|
|
help="Minutes before event for reminder [default: 30]",
|
|
|
|
metavar="MINUTES",
|
2021-05-28 05:03:46 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
parser.add_argument(
|
2021-05-28 05:05:11 -04:00
|
|
|
"--calendar",
|
|
|
|
dest="calendarID",
|
|
|
|
default="primary",
|
2021-05-28 05:03:46 -04:00
|
|
|
type=str,
|
2021-05-28 05:05:11 -04:00
|
|
|
action="store",
|
|
|
|
help="Calendar ID for the calendar you want to receive reminders from.",
|
2021-05-28 05:03:46 -04:00
|
|
|
)
|
2016-12-15 20:38:44 -05:00
|
|
|
|
2017-08-01 17:31:00 -04:00
|
|
|
options = parser.parse_args()
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
if not (options.zulip_email):
|
2021-05-28 05:05:11 -04:00
|
|
|
parser.error("You must specify --user")
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
zulip_client = zulip.init_from_options(options)
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2021-05-28 05:03:46 -04:00
|
|
|
|
2020-04-18 18:59:12 -04:00
|
|
|
def get_credentials() -> client.Credentials:
|
2016-12-15 20:38:44 -05:00
|
|
|
"""Gets valid user credentials from storage.
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
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:
|
2021-05-28 05:05:11 -04:00
|
|
|
credential_path = os.path.join(HOME_DIR, "google-credentials.json")
|
2016-12-15 20:38:44 -05:00
|
|
|
|
|
|
|
store = Storage(credential_path)
|
|
|
|
credentials = store.get()
|
|
|
|
|
|
|
|
return credentials
|
|
|
|
except client.Error:
|
2021-05-28 05:05:11 -04:00
|
|
|
logging.exception("Error while trying to open the `google-credentials.json` file.")
|
2020-04-09 20:14:01 -04:00
|
|
|
except OSError:
|
2016-12-15 20:38:44 -05:00
|
|
|
logging.error("Run the get-google-credentials script from this directory first.")
|
2012-10-21 16:18:51 -04:00
|
|
|
|
|
|
|
|
2020-04-18 18:59:12 -04:00
|
|
|
def populate_events() -> Optional[None]:
|
2017-02-25 03:21:20 -05:00
|
|
|
global events
|
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
credentials = get_credentials()
|
|
|
|
creds = credentials.authorize(httplib2.Http())
|
2021-05-28 05:05:11 -04:00
|
|
|
service = discovery.build("calendar", "v3", http=creds)
|
2016-12-15 20:38:44 -05:00
|
|
|
|
2017-02-28 01:26:51 -05:00
|
|
|
now = datetime.datetime.now(pytz.utc).isoformat()
|
2021-05-28 05:03:46 -04:00
|
|
|
feed = (
|
|
|
|
service.events()
|
|
|
|
.list(
|
|
|
|
calendarId=options.calendarID,
|
|
|
|
timeMin=now,
|
|
|
|
maxResults=5,
|
|
|
|
singleEvents=True,
|
2021-05-28 05:05:11 -04:00
|
|
|
orderBy="startTime",
|
2021-05-28 05:03:46 -04:00
|
|
|
)
|
|
|
|
.execute()
|
|
|
|
)
|
2016-12-15 20:38:44 -05:00
|
|
|
|
2017-02-25 03:21:20 -05:00
|
|
|
events = []
|
2016-12-15 20:38:44 -05:00
|
|
|
for event in feed["items"]:
|
|
|
|
try:
|
2017-02-25 03:16:52 -05:00
|
|
|
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.
|
2017-11-15 12:10:58 -05:00
|
|
|
start = event_timezone.localize(start)
|
2016-12-15 20:38:44 -05:00
|
|
|
except KeyError:
|
2017-02-25 03:16:52 -05:00
|
|
|
# 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.
|
2017-11-15 12:10:58 -05:00
|
|
|
start = calendar_timezone.localize(start_naive)
|
2017-02-25 03:16:52 -05:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
try:
|
2017-02-25 03:21:20 -05:00
|
|
|
events.append((event["id"], start, event["summary"]))
|
2016-12-15 20:38:44 -05:00
|
|
|
except KeyError:
|
2017-02-25 03:21:20 -05:00
|
|
|
events.append((event["id"], start, "(No Title)"))
|
2012-10-21 16:18:51 -04:00
|
|
|
|
|
|
|
|
2020-04-18 18:59:12 -04:00
|
|
|
def send_reminders() -> Optional[None]:
|
2012-10-21 16:18:51 -04:00
|
|
|
global sent
|
|
|
|
|
|
|
|
messages = []
|
|
|
|
keys = set()
|
2017-02-25 03:16:52 -05:00
|
|
|
now = datetime.datetime.now(tz=pytz.utc)
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
for id, start, summary in events:
|
2012-10-21 16:18:51 -04:00
|
|
|
dt = start - now
|
2016-12-15 20:38:44 -05:00
|
|
|
if dt.days == 0 and dt.seconds < 60 * options.interval:
|
2012-10-21 16:18:51 -04:00
|
|
|
# The unique key includes the start time, because of
|
|
|
|
# repeating events.
|
2016-12-15 20:38:44 -05:00
|
|
|
key = (id, start)
|
2012-10-21 16:18:51 -04:00
|
|
|
if key not in sent:
|
2016-12-15 20:38:44 -05:00
|
|
|
if start.hour == 0 and start.minute == 0:
|
2021-05-28 05:05:11 -04:00
|
|
|
line = "%s is today." % (summary,)
|
2016-12-15 20:38:44 -05:00
|
|
|
else:
|
2021-05-28 05:05:11 -04:00
|
|
|
line = "%s starts at %s" % (summary, start.strftime("%H:%M"))
|
|
|
|
print("Sending reminder:", line)
|
2012-10-21 16:18:51 -04:00
|
|
|
messages.append(line)
|
|
|
|
keys.add(key)
|
|
|
|
|
|
|
|
if not messages:
|
|
|
|
return
|
|
|
|
|
|
|
|
if len(messages) == 1:
|
2021-05-28 05:05:11 -04:00
|
|
|
message = "Reminder: " + messages[0]
|
2012-10-21 16:18:51 -04:00
|
|
|
else:
|
2021-05-28 05:05:11 -04:00
|
|
|
message = "Reminder:\n\n" + "\n".join("* " + m for m in messages)
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2021-05-28 05:03:46 -04:00
|
|
|
zulip_client.send_message(
|
2021-05-28 05:05:11 -04:00
|
|
|
dict(type="private", to=options.zulip_email, sender=options.zulip_email, content=message)
|
2021-05-28 05:03:46 -04:00
|
|
|
)
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2016-12-15 20:38:44 -05:00
|
|
|
sent.update(keys)
|
2012-10-21 16:18:51 -04:00
|
|
|
|
2021-05-28 05:03:46 -04:00
|
|
|
|
2012-10-21 16:18:51 -04:00
|
|
|
# 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:
|
2017-02-25 03:21:20 -05:00
|
|
|
populate_events()
|
2012-10-21 16:18:51 -04:00
|
|
|
send_reminders()
|
2017-03-05 04:25:27 -05:00
|
|
|
except Exception:
|
2016-12-15 20:38:44 -05:00
|
|
|
logging.exception("Couldn't download Google calendar and/or couldn't post to Zulip.")
|
2012-10-21 16:18:51 -04:00
|
|
|
time.sleep(60)
|