192 lines
		
	
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			192 lines
		
	
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
| #!/usr/bin/env python3
 | |
| #
 | |
| # This script depends on python-dateutil and python-pytz for properly handling
 | |
| # times and time zones of calendar events.
 | |
| import datetime
 | |
| import dateutil.parser
 | |
| import httplib2
 | |
| import itertools
 | |
| import logging
 | |
| import argparse
 | |
| import os
 | |
| import pytz
 | |
| import sys
 | |
| import time
 | |
| from typing import List, Optional, Set, Tuple
 | |
| 
 | |
| from oauth2client import client
 | |
| 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 = zulip.add_default_arguments(argparse.ArgumentParser(r"""
 | |
| 
 | |
| google-calendar --calendar calendarID@example.calendar.google.com
 | |
| 
 | |
|     This integration can be used to send yourself reminders, on Zulip, of Google Calendar Events.
 | |
| 
 | |
|     Specify your Zulip API credentials and server in a ~/.zuliprc file or using the options.
 | |
| 
 | |
|     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_argument('--interval',
 | |
|                     dest='interval',
 | |
|                     default=30,
 | |
|                     type=int,
 | |
|                     action='store',
 | |
|                     help='Minutes before event for reminder [default: 30]',
 | |
|                     metavar='MINUTES')
 | |
| 
 | |
| parser.add_argument('--calendar',
 | |
|                     dest = 'calendarID',
 | |
|                     default = 'primary',
 | |
|                     type = str,
 | |
|                     action = 'store',
 | |
|                     help = 'Calendar ID for the calendar you want to receive reminders from.')
 | |
| 
 | |
| options = parser.parse_args()
 | |
| 
 | |
| if not (options.zulip_email):
 | |
|     parser.error('You must specify --user')
 | |
| 
 | |
| zulip_client = zulip.init_from_options(options)
 | |
| 
 | |
| def get_credentials() -> 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 OSError:
 | |
|         logging.error("Run the get-google-credentials script from this directory first.")
 | |
| 
 | |
| 
 | |
| def populate_events() -> Optional[None]:
 | |
|     global events
 | |
| 
 | |
|     credentials = get_credentials()
 | |
|     creds = credentials.authorize(httplib2.Http())
 | |
|     service = discovery.build('calendar', 'v3', http=creds)
 | |
| 
 | |
|     now = datetime.datetime.now(pytz.utc).isoformat()
 | |
|     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)
 | |
|         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)
 | |
| 
 | |
|         try:
 | |
|             events.append((event["id"], start, event["summary"]))
 | |
|         except KeyError:
 | |
|             events.append((event["id"], start, "(No Title)"))
 | |
| 
 | |
| 
 | |
| def send_reminders() -> 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 Exception:
 | |
|         logging.exception("Couldn't download Google calendar and/or couldn't post to Zulip.")
 | |
|     time.sleep(60)
 | 
