api: Move the API package to a dedicated subdirectory.
In order to keep all three packages (zulip, zulip_bots, zulip_botserver) in the same repo, all package files must now be nested one level deeper. For instance, python-zulip-api/zulip_bots/zulip_bots/bots/, instead of python-zulip-api/zulip_bots/bots/.
This commit is contained in:
parent
879f44ab3a
commit
3d0f7955b6
59 changed files with 186 additions and 192 deletions
56
zulip/integrations/google/get-google-credentials
Normal file
56
zulip/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()
|
200
zulip/integrations/google/google-calendar
Executable file
200
zulip/integrations/google/google-calendar
Executable file
|
@ -0,0 +1,200 @@
|
|||
#!/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.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) # 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 Exception:
|
||||
logging.exception("Couldn't download Google calendar and/or couldn't post to Zulip.")
|
||||
time.sleep(60)
|
Loading…
Add table
Add a link
Reference in a new issue