Skip to content

Update Google Calendar integration #856

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
352a475
google-calendar: Update requirements.txt dependencies.
Niloth-p Feb 18, 2025
81387d1
google-calendar: Replace deprecated oauth2client library.
Niloth-p Feb 18, 2025
e722e90
google-calendar: Update outdated send_message parameters.
Niloth-p Feb 18, 2025
dfe38f1
google-calendar: Narrow the permission scope to calendar events.
Niloth-p Feb 18, 2025
4b9dbb3
google-calendar: Use a constant for the tokens filename.
Niloth-p Feb 18, 2025
5b8b440
google-calendar: Fix usage of term "credentials", replace with "tokens".
Niloth-p Feb 18, 2025
2bb1b8a
google-calendar: Update command usage help.
Niloth-p Feb 18, 2025
f30a571
google-calendar: Clean up `add_argument` parameters.
Niloth-p Feb 18, 2025
f18399c
google-calendar: Add --provision argument to install dependencies.
Niloth-p Feb 18, 2025
07810c5
google-calendar: Add error handling for missing client secret file.
Niloth-p Feb 18, 2025
abf6415
google-calendar: Stop printing events unless the verbose option is set.
Niloth-p Feb 18, 2025
8b2b9dd
google-calendar: Add error handling for send_message.
Niloth-p Feb 18, 2025
06db61b
google-calendar: Improve CLIENT_SECRET_FILE occurrences.
Niloth-p Feb 18, 2025
445ee9b
google-calendar: Call get-google-credentials script internally.
Niloth-p Feb 18, 2025
8b7c536
google-calendar: Use current user's email id for send_message.
Niloth-p Feb 18, 2025
6436918
google-calendar: Use a bot to send direct messages to the bot owner.
Niloth-p Feb 18, 2025
5d3ff39
google-calendar: Support sending reminders to channels.
Niloth-p Feb 18, 2025
4c56d9b
google-calendar: Support manual authorization using auth code.
Niloth-p Feb 18, 2025
441dce9
google-calendar: Log writing to the tokens file.
Niloth-p Feb 18, 2025
c004199
google-calendar: Add options --client-secret-file and --tokens-file.
Niloth-p Feb 18, 2025
9ced7b8
google-calendar: Support loading options from the zuliprc.
Niloth-p Feb 19, 2025
e38a8e6
google-calendar: Fix type of event id, switch from int to str.
Niloth-p Feb 19, 2025
da6d896
google-calendar: Send each reminder as its own message.
Niloth-p Feb 19, 2025
8794800
google-calendar: Add TypedDict and string conversion function for event.
Niloth-p Feb 19, 2025
b468931
google-calendar: Generalize the datetime parsing for event fields.
Niloth-p Feb 19, 2025
078d307
google-calendar: Display more Event info in reminder messages.
Niloth-p Feb 19, 2025
974863e
google-calendar: Support user customization of the message template.
Niloth-p Feb 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ module = [
"apiai.*",
"feedparser.*",
"gitlint.*",
"google.auth.*",
"google.oauth2.*",
"google_auth_oauthlib.*",
"googleapiclient.*",
"irc.*",
"mercurial.*",
Expand Down
64 changes: 35 additions & 29 deletions zulip/integrations/google/get-google-credentials
Original file line number Diff line number Diff line change
@@ -1,46 +1,52 @@
#!/usr/bin/env python3
import argparse
import os

from oauth2client import client, tools
from oauth2client.file import Storage
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow

flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()

# 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"
SCOPES = ["https://www.googleapis.com/auth/calendar.events.readonly"]
# File containing user's access and refresh tokens for Google application requests.
# If it does not exist, e.g., first run, it is generated on user authorization.
TOKENS_FILE = "google-tokens.json"
# 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" # noqa: S105
APPLICATION_NAME = "Zulip Calendar Bot"
HOME_DIR = os.path.expanduser("~")


def get_credentials() -> client.Credentials:
"""Gets valid user credentials from storage.
def get_credentials() -> Credentials:
"""
Writes google tokens to a json file, using the client secret file (for the OAuth flow),
and the refresh token.

If the tokens file exists and is valid, nothing needs to be done.
If the tokens file exists, but the auth token is expired (expiry duration of auth token
is 1 hour), the refresh token is used to get a new token.
If the tokens file does not exist, or is invalid, the OAuth2 flow is triggered.

If nothing has been stored, or if the stored credentials are invalid,
the OAuth2 flow is completed to obtain the new credentials.
The OAuth2 flow needs the client secret file, and requires the user to grant access to
the application via a browser authorization page, for the first run.

Returns:
Credentials, the obtained credential.
The fetched tokens are written to storage in a json file, for reference by other scripts.
"""

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
# 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)
print("Storing credentials to " + credential_path)
creds = None
tokens_path = os.path.join(HOME_DIR, TOKENS_FILE)

if os.path.exists(tokens_path):
creds = Credentials.from_authorized_user_file(tokens_path, SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
os.path.join(HOME_DIR, CLIENT_SECRET_FILE), SCOPES
)
creds = flow.run_local_server(port=0)
with open(tokens_path, "w") as token:
token.write(creds.to_json())
return creds


get_credentials()
58 changes: 22 additions & 36 deletions zulip/integrations/google/google-calendar
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
#!/usr/bin/env python3
#
# This script depends on python-dateutil and python-pytz for properly handling
# times and time zones of calendar events.
import argparse
import datetime
import itertools
Expand All @@ -12,23 +9,23 @@ import time
from typing import List, Optional, Set, Tuple

import dateutil.parser
import httplib2
import pytz
from oauth2client import client
from oauth2client.file import Storage

try:
from googleapiclient import discovery
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
except ImportError:
logging.exception("Install google-api-python-client")
logging.exception("Install the required python packages from requirements.txt first.")
sys.exit(1)

sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
import zulip

SCOPES = "https://www.googleapis.com/auth/calendar.readonly"
SCOPES = ["https://www.googleapis.com/auth/calendar.events.readonly"]
# File containing user's access and refresh tokens for Google application requests.
# If it does not exist, e.g., first run, it is generated on user authorization.
TOKENS_FILE = "google-tokens.json"
CLIENT_SECRET_FILE = "client_secret.json" # noqa: S105
APPLICATION_NAME = "Zulip"
HOME_DIR = os.path.expanduser("~")

# Our cached view of the calendar, updated periodically.
Expand All @@ -39,26 +36,20 @@ sent: Set[Tuple[int, datetime.datetime]] = set()

sys.path.append(os.path.dirname(__file__))

parser = zulip.add_default_arguments(
argparse.ArgumentParser(
r"""
usage = r"""google-calendar --user EMAIL [--interval MINUTES] [--calendar CALENDAR_ID]

google-calendar --calendar [email protected]
This integration can be used to send Zulip messages as reminders for upcoming events from your Google Calendar.

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.

Specify your Zulip API credentials and server in a ~/.zuliprc file or using the options.
Before running this integration, make sure you download the client secret file from Google, and run the get-google-credentials script to give Zulip read access to your Google Calendar.

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, as your API key is accessible to local users through the command line.

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
For more information, see https://zulip.com/integrations/doc/google-calendar.
"""
)
)

parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))


parser.add_argument(
Expand Down Expand Up @@ -88,7 +79,7 @@ if not options.zulip_email:
zulip_client = zulip.init_from_options(options)


def get_credentials() -> client.Credentials:
def get_credentials() -> Credentials:
"""Gets valid user credentials from storage.

If nothing has been stored, or if the stored credentials are invalid,
Expand All @@ -99,12 +90,10 @@ def get_credentials() -> client.Credentials:
Credentials, the obtained credential.
"""
try:
credential_path = os.path.join(HOME_DIR, "google-credentials.json")

store = Storage(credential_path)
return store.get()
except client.Error:
logging.exception("Error while trying to open the `google-credentials.json` file.")
tokens_path = os.path.join(HOME_DIR, TOKENS_FILE)
return Credentials.from_authorized_user_file(tokens_path, SCOPES)
except ValueError:
logging.exception("Error while trying to open the %s file.", TOKENS_FILE)
sys.exit(1)
except OSError:
logging.error("Run the get-google-credentials script from this directory first.")
Expand All @@ -113,8 +102,7 @@ def get_credentials() -> client.Credentials:

def populate_events() -> Optional[None]:
credentials = get_credentials()
creds = credentials.authorize(httplib2.Http())
service = discovery.build("calendar", "v3", http=creds)
service = build("calendar", "v3", credentials=credentials)

now = datetime.datetime.now(pytz.utc).isoformat()
feed = (
Expand Down Expand Up @@ -186,9 +174,7 @@ def send_reminders() -> Optional[None]:
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)
)
zulip_client.send_message({"type": "direct", "to": [options.zulip_email], "content": message})

sent.update(keys)

Expand Down
7 changes: 5 additions & 2 deletions zulip/integrations/google/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
httplib2>=0.22.0
oauth2client>=4.1.3
google-api-python-client>=1.7.9
google-auth-httplib2>=0.0.3
google-auth-oauthlib>=0.4.0
python-dateutil
pytz