Skip to content

Commit

Permalink
Merge pull request #38 from minds-ai/switch_to_oauth
Browse files Browse the repository at this point in the history
Switch to Zoom OAuth access
  • Loading branch information
jbedorf committed Aug 8, 2023
2 parents bf68adc + ea17cf1 commit 8aa01b4
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 53 deletions.
56 changes: 27 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# Zoom-Drive-Connector

This program downloads meeting recordings from Zoom and uploads them to a
This program downloads meeting recordings from Zoom and uploads them to a
specific folder on Google Drive.

This software is particularly helpful for archiving recorded meetings and
This software is particularly helpful for archiving recorded meetings and
webinars on Zoom. It can also be used to distribute VODs (videos on demand) to
public folder in Google Drive. This software aims to fill a missing piece of
public folder in Google Drive. This software aims to fill a missing piece of
functionality in Zoom (post-meeting recording sharing).

## Setup
Create a file called `config.yaml` with the following contents:
```yaml
zoom:
key: "zoom_api_key"
secret: "zoom_api_secret"
username: "[email protected]"
account_id: "Zoom account ID"
client_id: "OAuth client ID"
client_secret: "OAuth client secret"
delete: true
meetings:
meetings:
- {id: "meeting_id" , name: "Meeting Name", folder_id: "Some Google Drive Folder ID", slack_channel: "channel_name"}
- {id: "meeting_id2" , name: "Second Meeting Name", folder_id: "Some Google Drive Folder ID2", slack_channel: "channel_name2"}
drive:
Expand All @@ -26,23 +26,21 @@ slack:
key: "slack_api_key"
internals:
target_folder: "/tmp"
```
```
*Note:* It is advised to place this file in the `conf` folder (together with the json credentials)
this folder needs to be referenced when you launch the Docker container (see below).

You will need to fill in the example values in the file above. In order to
You will need to fill in the example values in the file above. In order to
fill in some of these values you will need to create developer credentials on
several services. Short guides on each service can be found below.

### Zoom
To get the proper API key and secret, you will need to use the following
"Getting Started Guide" on Zoom's developer site. Link [here](https://developer.zoom.us/docs/windows/introduction-and-pre-requisite/).
Paste the API key and secret into `config.yaml` under the `zoom` section.
https://developers.zoom.us/docs/internal-apps/create/#steps-to-create-a-server-to-server-oauth-app

It is (currently) not possible to download the recordings via the API key only.
As such you need to specify a user that is part of your organizations Zoom account.
This can be a non-pro user. The e-mail of the user should be added
to the `username` entry under the `zoom` section.
For Zoom the Server-to-Server OAuth app is used. You will need to use the following
[guide](https://developer.zoom.us/docs/windows/introduction-and-pre-requisite/). on the Zoom's
developer site. You need to copy the 'account_id', 'client_id' and 'client_secret' variables into
the `config.yaml` file under the `zoom` section.

### Google Drive
To upload files to Google Drive you have to login to your developer console, create a new project,
Expand All @@ -52,15 +50,15 @@ steps:
1. Go to the [Google API Console](https://console.developers.google.com/)
2. Click on "Create new project".
3. Give it a name and enter any other required info.
4. Once back on the dashboard click on "Enable APIs and Services" (make sure your newly
4. Once back on the dashboard click on "Enable APIs and Services" (make sure your newly
created project is selected).
5. Search for and enable: "Google Drive API".
6. Go back to the dashboard and click on "Credentials" in the left side bar.
7. On the credentials screen click on the "Create credentials" button and select "OAuth client ID".
8. Follow the instructions to set a product name, on the next screen only the `Application name`
is required. Enter it and click on "save".
9. As application type, select "Other" and fill in the name of your client.
10. Now under "OAuth 2.0 client IDs" download the JSON file for the newly create client ID
10. Now under "OAuth 2.0 client IDs" download the JSON file for the newly create client ID
11. Save this file as `client_secrets.json` in the `conf` directory.

The `credentials` file will be created during the first start (see below).
Expand All @@ -75,9 +73,9 @@ The `credentials` file will be created during the first start (see below).

## Running the Program
The first time we run the program we have to authenticate it with Google and accept the required
permissions. For this we run the docker container in the interactive mode such that we
permissions. For this we run the docker container in the interactive mode such that we
can enter the generated token. Instructions on how to pull Docker images from the Github
registry can be found
registry can be found
[here](https://help.github.com/en/articles/configuring-docker-for-use-with-github-package-registry#authenticating-to-github-package-registry).

```bash
Expand All @@ -86,14 +84,14 @@ $ docker run -i -v /path/to/conf/directory:/conf \
docker.pkg.github.com/minds-ai/zoom-drive-connector/zoom-drive-connector:1.2.0
```

Alternatively, you can clone the repository and run `make build VERSION=1.2.0` to build
Alternatively, you can clone the repository and run `make build VERSION=1.2.0` to build
the container locally. In this case, you will need to exchange the long image name
with the short-form one (`zoom-drive-connector:1.2.0`).

This will print an URL, this URL should be copied in the browser. After accepting the
permissions you will be presented with a token. This token should be pasted in the
terminal. After the token has been accepted a `credentials.json` file will have been
created in your configuration folder. You can now kill (`ctrl-C`) the Docker container
This will print an URL, this URL should be copied in the browser. After accepting the
permissions you will be presented with a token. This token should be pasted in the
terminal. After the token has been accepted a `credentials.json` file will have been
created in your configuration folder. You can now kill (`ctrl-C`) the Docker container
and follow the steps below to run it in the background.

Run the following command to start the container after finishing the setup process.
Expand All @@ -103,17 +101,17 @@ $ docker run -d -v /path/to/conf/directory:/conf \
```

## Making Changes to Source
If you wish to make changes to the program source, you can quickly create a
If you wish to make changes to the program source, you can quickly create a
Conda environment using the provided `environment.yml` file. Use the following
commands to get started.
```bash
$ conda env create -f environment.yml
$ source activate zoom-drive-connector
```
```

Any changes to dependencies should be recorded in **both** `environment.yml` and
`requirements.txt` with the exception of development dependencies, which
only have to be placed in `environment.yml`. Make sure to record the version of the
`requirements.txt` with the exception of development dependencies, which
only have to be placed in `environment.yml`. Make sure to record the version of the
package you are adding using the double-equal operator.

To run the program in the conda environment you can use the following command line:
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pyjwt==1.5.3
pyyaml>=5.1
slackclient==1.2.1
schedule==0.5.0
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name='zoom-drive-connector',
version='1.4',
version='1.7',
packages=find_packages(exclude=['tests']),
url='https://github.com/minds-ai/zoom-drive-connector',
license='Apache 2.0',
Expand All @@ -20,7 +20,6 @@
long_description_content_type='text/markdown',
# Package requirements.
install_requires=[
'pyjwt==1.5.3',
'pyyaml>=5.1',
'slackclient==1.2.1',
'schedule==0.5.0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ def validate(self) -> bool:
:return: Checks to make sure that the meetings have all required properties
"""

if not all(
k in self.settings_dict for k in ('key', 'secret', 'username', 'delete', 'meetings')):
k in self.settings_dict for k in (
'account_id', 'client_id', 'client_secret', 'delete', 'meetings')
):
return False

for meeting in self.settings_dict['meetings']:
Expand Down
64 changes: 45 additions & 19 deletions zoom_drive_connector/zoom/zoom_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@
import datetime
from enum import Enum
import os
import time
import shutil
import logging
from typing import TypeVar, cast, Dict, Any

import requests
import jwt
from requests.auth import HTTPBasicAuth

from zoom_drive_connector.configuration import APIConfigBase, ZoomConfig, SystemConfig

Expand All @@ -37,6 +36,7 @@ class ZoomURLS(Enum):
zak_token = 'https://api.zoom.us/v2/users/{user}/token?type=zak'
delete_recordings = 'https://api.zoom.us/v2/meetings/{id}/recordings/{rid}'
signin = 'https://api.zoom.us/signin'
oauth_token = 'https://zoom.us/oauth/token'


class ZoomAPI:
Expand All @@ -58,42 +58,64 @@ def __init__(self, zoom_config: S, sys_config: S):
409: 'File deleted already.'
}

def generate_jwt(self) -> bytes:
"""Generates the JSON web token used for authenticating with Zoom. Sends client key
and expiration time encoded with the secret key.
def generate_server_to_server_oath_token(self) -> bytes:
"""Generates the OATH token used for authenticating with Zoom.
Sends OATH information and receives a token to use for the next hour.
"""
payload = {'iss': self.zoom_config.key, 'exp': int(time.time()) + self.timeout}
return jwt.encode(payload, str(self.zoom_config.secret), algorithm='HS256')
data = {
"grant_type" : "account_credentials",
"account_id" : self.zoom_config.account_id
}
headers = {
'content-type': 'application/x-www-form-urlencoded'
}
res = requests.post(
ZoomURLS.oauth_token.value,
headers=headers,
params=data,
auth=HTTPBasicAuth(self.zoom_config.client_id, self.zoom_config.client_secret)
)
if res.status_code != 200:
raise ValueError("Failed to authenticate, error: ", res.json())
return res.json()["access_token"]

def delete_recording(self, meeting_id: str, recording_id: str, auth: bytes):
"""Given a specific meeting room ID and recording ID, this function moves the recording to the
trash in Zoom's cloud.
:param meeting_id: UUID associated with a meeting room.
:param recording_id: The ID of the recording to trash.
:param auth: JWT token.
:param auth: OAUTH token.
"""
zoom_url = str(ZoomURLS.delete_recordings.value).format(id=meeting_id, rid=recording_id)
res = requests.delete(
zoom_url, params={'access_token': auth,
'action': 'trash'}) # trash, not delete
headers = {
'authorization': 'Bearer ' + auth,
'content-type': 'application/json'
}
# trash, not delete
res = requests.delete(zoom_url, headers=headers, params={'action': 'trash'})
status_code = res.status_code
if 400 <= status_code <= 499:
raise ZoomAPIException(status_code, res.reason, res.request, self.message.get(
status_code, ''))

def get_recording_url(self, meeting_id: str, auth: bytes) -> Dict[str, Any]:
def get_recording_url(self, meeting_id: str, auth: str) -> Dict[str, Any]:
"""Given a specific meeting room ID and auth token, this function gets the download url
for most recent recording in the given meeting room.
:param meeting_id: UUID associated with a meeting room.
:param auth: Encoded JWT authorization token
:param auth: Authorization token
:return: dict containing the date of the recording, the ID of the recording, and the video url.
"""
zoom_url = str(ZoomURLS.recordings.value).format(id=meeting_id)

try:
zoom_request = requests.get(zoom_url, params={'access_token': auth})
headers = {
'authorization': 'Bearer ' + auth,
'content-type': 'application/json'
}
zoom_request = requests.get(zoom_url, headers=headers)
except requests.exceptions.RequestException as e:
# Failed to make a connection so let's just return a 404, as there is no file
# but print an additional warning in case it was a configuration error
Expand Down Expand Up @@ -126,15 +148,18 @@ def get_recording_url(self, meeting_id: str, auth: bytes) -> Dict[str, Any]:
else:
raise ZoomAPIException(status_code, zoom_request.reason, zoom_request.request, '')

def download_recording(self, url: str, auth: bytes) -> str:
def download_recording(self, url: str, auth: str) -> str:
"""Downloads video file from Zoom to local folder.
:param url: Download URL for meeting recording.
:param auth: Encoded JWT authorization token
:param auth: Authorization token.
:return: Path to the recording
"""
zoom_request = requests.get(url, stream=True, params={'access_token': auth})

headers = {
'authorization': 'Bearer ' + auth,
'content-type': 'application/json'
}
zoom_request = requests.get(url, stream=True, headers=headers)

filename = url.split('/')[-1]
outfile = os.path.join(str(self.sys_config.target_folder), filename + '.mp4')
Expand All @@ -155,8 +180,9 @@ def pull_file_from_zoom(self, meeting_id: str, rm: bool = True) -> Dict[str, Any
"""
result = {'success': False, 'date': None, 'filename': None}
try:
log.log(logging.INFO, f'Found recording for meeting {meeting_id} starting download...')
# Generate token and Authorization header.
zoom_token = self.generate_jwt()
zoom_token = self.generate_server_to_server_oath_token()

# Get URL and download the file.
res = self.get_recording_url(meeting_id, zoom_token)
Expand Down

0 comments on commit 8aa01b4

Please sign in to comment.