Last active
August 16, 2021 06:48
-
-
Save infojunkie/704508f9c0a55999f9b1418844e02682 to your computer and use it in GitHub Desktop.
Upload Zoom files to AWS S3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
service: labyrinth-service | |
provider: | |
name: aws | |
iamManagedPolicies: | |
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" | |
iamRoleStatements: | |
- Effect: Allow | |
Action: | |
- lambda:InvokeFunction | |
Resource: | |
- "*" | |
functions: | |
# Responsible for receiving Zoom webhook and invoking ZoomUploadAsync | |
zoomUpload: | |
handler: zoomUpload.handler | |
events: | |
- http: | |
path: /media/zoom | |
method: post | |
environment: | |
STAGE: "${self:custom.stage}" | |
REGION: "${self:custom.region}" | |
# Responsible for uploading Zoom video to watch bucket | |
zoomUploadAsync: | |
handler: zoomUploadAsync.handler | |
timeout: 120 | |
environment: | |
DESTINATION_BUCKET: "${self:custom.uploadBucketName}" | |
ZOOM_API_KEY: "${self:custom.secretParams.ZOOM_API_KEY}" | |
ZOOM_API_SECRET: "${self:custom.secretParams.ZOOM_API_SECRET}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
exports.handler = async (event, context) => { | |
try { | |
const phScript = new ZoomUpload({ event, context }); | |
return await phScript.main(); | |
} catch (e) { | |
throw e; | |
} | |
}; | |
const AWS = require('aws-sdk'); | |
const _ = require('lodash'); | |
const { inspect } = require('util'); | |
class ZoomUpload { | |
constructor(props = {}) { | |
this._event = props.event; | |
this._data = JSON.parse(this._event.body||'{}'); | |
} | |
async main() { | |
// TODO Verify Zoom `authorization` header as per https://marketplace.zoom.us/docs/api-reference/webhook-reference#headers | |
if (this._data.event !== 'recording.completed') { | |
console.warn(`Received Zoom event ${this._data.event}. Ignoring.`); | |
return; | |
} | |
const code = this.getCourseCode(); | |
if (!code) { | |
console.warn(`Could not find course code in meeting "${this._data.payload.object.topic}". Ignoring.`); | |
return; | |
} | |
const videos = this.getVideoFiles(); | |
if (!videos.length) { | |
console.warn(`Could not find any eligible video in meeting "${this._data.payload.object.topic}". Ignoring.`); | |
return; | |
} | |
const fn = `labyrinth-service-${process.env['STAGE']}-zoomUploadAsync`; | |
for (const video of videos) { | |
try { | |
await this.invokeLambda(fn, video, code); | |
} | |
catch (error) { | |
console.error(`Error occurred while invoking upload function ${fn} for meeting "${this._data.payload.object.topic}": ${error}`); | |
} | |
} | |
} | |
async invokeLambda(fn, video, code) { | |
return new Promise((resolve, reject) => { | |
const lambda = new AWS.Lambda({ region: process.env['REGION'] }); | |
lambda.invoke({ FunctionName: fn, InvocationType: 'Event', Payload: JSON.stringify({ | |
topic: this._data.payload.object.topic, | |
code, | |
video | |
})}, (error, result) => { | |
if (error) { | |
reject(error); | |
} | |
else { | |
resolve(result); | |
} | |
}); | |
}); | |
} | |
// Detect if this is a course meeting having a [code123] substring. | |
getCourseCode() { | |
const code = _.get(this._data, 'payload.object.topic', '').match(/\[(\w+)\]/); | |
return code && code[1]; | |
} | |
// Detect video files that we want to upload. | |
getVideoFiles() { | |
return _.get(this._data, 'payload.object.recording_files', []).filter(file => { | |
const start = new Date(file.recording_start); | |
const end = new Date(file.recording_end); | |
// Return mp4 videos with running time >= 2min | |
return file.file_type.toUpperCase() === "MP4" | |
&& end - start >= 1000*60*2 | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ports.handler = async (event, context) => { | |
try { | |
const phScript = new ZoomUploadAsync({ event, context }); | |
return await phScript.main(); | |
} catch (e) { | |
throw e; | |
} | |
}; | |
const fetch = require('node-fetch'); | |
const jwt = require('jsonwebtoken'); | |
const AWS = require('aws-sdk'); | |
const _ = require('lodash'); | |
const { inspect } = require('util'); | |
// https://stackoverflow.com/a/10075654/209184 | |
function padDigits(number, digits) { | |
return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; | |
} | |
const PREFIX = 'courses/uploads'; | |
class ZoomUploadAsync { | |
constructor(props = {}) { | |
this._event = props.event; | |
} | |
async main() { | |
const video = this._event.video; | |
try { | |
await this.uploadZoomToS3( | |
video.download_url, | |
video.file_size, | |
this.recordingTofilename(), | |
`${PREFIX}/${this._event.code}` | |
); | |
} | |
catch (error) { | |
console.error(`Error occurred while uploading video at ${video.play_url} for meeting "${this._event.topic}": ${error}`); | |
} | |
} | |
async uploadZoomToS3(zoomDownloadUrl, size, fileName, prefix) { | |
const zoomToken = this.generateZoomToken(); | |
return new Promise((resolve, reject) => { | |
fetch(`${zoomDownloadUrl}?access_token=${zoomToken}`, { | |
method: 'GET', | |
redirect: 'follow' | |
}) | |
.then(response => { | |
const s3 = new AWS.S3(); | |
const request = s3.putObject({ | |
Bucket: process.env['DESTINATION_BUCKET'], | |
Key: `${prefix}/${fileName}`, | |
Body: response.body, | |
ContentType: 'video/mp4', | |
ContentLength: size || Number(response.headers.get('content-length')) | |
}); | |
return request.promise(); | |
}) | |
.then(data => { | |
console.log(`Successfully uploaded ${fileName} to ${prefix}.`); | |
resolve(data); | |
}); | |
}); | |
} | |
generateZoomToken() { | |
const zoomPayload = { | |
iss: process.env['ZOOM_API_KEY'], | |
exp: ((new Date()).getTime() + 5000) | |
}; | |
return jwt.sign(zoomPayload, process.env['ZOOM_API_SECRET']); | |
} | |
recordingTofilename() { | |
// GMT20210429_165119_Recording.mp4 | |
const video = this._event.video; | |
const date = new Date(video.recording_start); | |
return 'GMT' + | |
date.getUTCFullYear() + | |
padDigits(date.getUTCMonth()+1, 2) + | |
padDigits(date.getUTCDate(), 2) + | |
'_' + | |
padDigits(date.getUTCHours(), 2) + | |
padDigits(date.getUTCMinutes(), 2) + | |
padDigits(date.getUTCSeconds(), 2) + | |
'_Recording.mp4'; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment