diff --git a/pyproject.toml b/pyproject.toml index 159c85e23..5261b00f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,9 @@ module = [ "feedparser.*", "gitlint.*", "googleapiclient.*", + "google_api_python_client.*", + "google_auth_httplib2.*", + "google_auth_oauthlib.*", "irc.*", "mercurial.*", "nio.*", diff --git a/requirements.txt b/requirements.txt index c5d735436..e0e805d3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ types-pytz types-requests gitlint>=0.13.0 -r ./zulip/integrations/bridge_with_matrix/requirements.txt +-r ./zulip/integrations/google/requirements.txt diff --git a/zulip/integrations/google/get-google-credentials b/zulip/integrations/google/get-google-credentials index bb97e5f69..00f9ca9a8 100755 --- a/zulip/integrations/google/get-google-credentials +++ b/zulip/integrations/google/get-google-credentials @@ -2,24 +2,32 @@ 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() +flags = argparse.ArgumentParser(description="Google Calendar Bot") +flags.add_argument( + "--noauth_local_webserver", + action="store_true", + help="Run OAuth flow in console instead of opening a web browser.", +) +args = flags.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.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" # noqa: S105 APPLICATION_NAME = "Zulip Calendar Bot" HOME_DIR = os.path.expanduser("~") +CREDENTIALS_PATH = os.path.join(HOME_DIR, "google-credentials.json") -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, @@ -29,18 +37,31 @@ def get_credentials() -> client.Credentials: Credentials, the obtained credential. """ - credential_path = os.path.join(HOME_DIR, "google-credentials.json") + creds = None - 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) + # Check if the credentials file exists + if os.path.exists(CREDENTIALS_PATH): + creds = Credentials.from_authorized_user_file(CREDENTIALS_PATH, SCOPES) + # If there are no valid credentials, initiate the OAuth flow + 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 + ) + if args.noauth_local_webserver: + creds = flow.run_console() + else: + creds = flow.run_local_server(port=0) + + # Save the credentials for future use + with open(CREDENTIALS_PATH, "w") as token_file: + token_file.write(creds.to_json()) + + print("Storing credentials to " + CREDENTIALS_PATH) + + return creds get_credentials() diff --git a/zulip/integrations/google/google-calendar b/zulip/integrations/google/google-calendar index 85906bd46..d56641068 100755 --- a/zulip/integrations/google/google-calendar +++ b/zulip/integrations/google/google-calendar @@ -12,21 +12,16 @@ 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 -except ImportError: - logging.exception("Install google-api-python-client") - sys.exit(1) +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build 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.readonly"] CLIENT_SECRET_FILE = "client_secret.json" # noqa: S105 APPLICATION_NAME = "Zulip" HOME_DIR = os.path.expanduser("~") @@ -88,33 +83,40 @@ 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, - an exception is thrown and the user is informed to run the script in this directory to get - credentials. + the user will be prompted to authenticate. Returns: Credentials, the obtained credential. """ - try: - credential_path = os.path.join(HOME_DIR, "google-credentials.json") + credential_path = os.path.join(HOME_DIR, "google-credentials.json") + creds = None + + # Load credentials from file if they exist + if os.path.exists(credential_path): + creds = Credentials.from_authorized_user_file(credential_path, SCOPES) + + # If there are no (valid) credentials available, prompt the user to log in. + 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(CLIENT_SECRET_FILE, SCOPES) + creds = flow.run_local_server(port=0) + + # Save the credentials for the next run + with open(credential_path, "w") as token: + token.write(creds.to_json()) - store = Storage(credential_path) - return store.get() - except client.Error: - logging.exception("Error while trying to open the `google-credentials.json` file.") - sys.exit(1) - except OSError: - logging.error("Run the get-google-credentials script from this directory first.") - sys.exit(1) + return creds 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 = ( diff --git a/zulip/integrations/google/requirements.txt b/zulip/integrations/google/requirements.txt index 139c0705b..018523c01 100644 --- a/zulip/integrations/google/requirements.txt +++ b/zulip/integrations/google/requirements.txt @@ -1,2 +1,3 @@ -httplib2>=0.22.0 -oauth2client>=4.1.3 +google-api-python-client>=2.157.0 +google-auth-httplib2>=0.2.0 +google-auth-oauthlib>=1.2.1