UNPKG

@uppy/companion

Version:

OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:

200 lines (199 loc) 8.57 kB
var _a; import got from 'got'; import moment from 'moment-timezone'; import pMap from 'p-map'; import { getBasicAuthHeader, prepareStream } from '../../helpers/utils.js'; import Provider from '../Provider.js'; import { withProviderErrorHandling } from '../providerErrors.js'; import adaptData from './adapter.js'; const BASE_URL = 'https://zoom.us/v2'; const PAGE_SIZE = 300; const DEAUTH_EVENT_NAME = 'app_deauthorized'; const getClient = ({ token }) => got.extend({ prefixUrl: BASE_URL, headers: { authorization: `Bearer ${token}`, }, }); async function findFile({ client, meetingId, fileId, recordingStart }) { const { recording_files: files } = await client .get(`meetings/${encodeURIComponent(meetingId)}/recordings`, { responseType: 'json', }) .json(); return files.find((file) => fileId === file.id || (file.file_type === fileId && file.recording_start === recordingStart)); } /** * Adapter for API https://marketplace.zoom.us/docs/api-reference/zoom-api */ class Zoom extends Provider { static get oauthProvider() { return 'zoom'; } async list(options) { return this.#withErrorHandling('provider.zoom.list.error', async () => { const { providerUserSession: { accessToken: token }, } = options; const query = options.query || {}; const meetingId = options.directory || ''; const requestedYear = query.year ? parseInt(query.year, 10) : null; const client = getClient({ token }); const user = await client.get('users/me', { responseType: 'json' }).json(); const { timezone } = user; const userTz = timezone || 'UTC'; if (meetingId) { const recordingInfo = await client .get(`meetings/${encodeURIComponent(meetingId)}/recordings`, { responseType: 'json', }) .json(); return adaptData(user, recordingInfo); } if (requestedYear) { const now = moment.tz(userTz); const numMonths = now.get('year') === requestedYear ? now.get('month') + 1 : 12; const monthsToCheck = Array.from({ length: numMonths }, (_, i) => i); // in moment, months are 0-indexed // Run each month in parallel: const allMeetingsInYear = (await pMap(monthsToCheck, async (month) => { const startDate = moment .tz({ year: requestedYear, month, day: 1 }, userTz) .startOf('month'); const endDate = startDate.clone().endOf('month'); const searchParams = { page_size: PAGE_SIZE, from: startDate.clone().tz('UTC').format('YYYY-MM-DD'), to: endDate.clone().tz('UTC').format('YYYY-MM-DD'), }; const paginatedMeetings = []; do { const currentChunkMeetingsInfo = await client .get('users/me/recordings', { searchParams, responseType: 'json', }) .json(); paginatedMeetings.push(...(currentChunkMeetingsInfo.meetings ?? [])); searchParams.next_page_token = currentChunkMeetingsInfo.next_page_token; } while (searchParams.next_page_token); return paginatedMeetings; }, { concurrency: 3 })).flat(); // this is effectively a flatMap // concurrency 3 seems like a sensible number... const finalResult = { meetings: allMeetingsInYear }; return adaptData(user, finalResult); } const accountCreationDate = moment.utc(user.created_at); const startYear = accountCreationDate.year(); const currentYear = moment.tz(userTz).year(); const years = []; for (let year = currentYear; year >= startYear; year--) { years.push({ isFolder: true, icon: 'folder', name: `${year}`, mimeType: null, id: `${year}`, thumbnail: null, requestPath: `?year=${year}`, modifiedDate: `${year}-12-31`, // Representative date size: null, }); } return { username: user.email, items: years, nextPagePath: null, }; }); } async download({ id: meetingId, providerUserSession: { accessToken: token }, query, }) { return this.#withErrorHandling('provider.zoom.download.error', async () => { // meeting id + file id required // cc files don't have an ID or size const { recordingStart, recordingId: fileId } = query; const client = getClient({ token }); const foundFile = await findFile({ client, meetingId, fileId, recordingStart, }); const url = foundFile?.download_url; if (!url) throw new Error('Download URL not found'); const stream = client.stream.get(`${url}?access_token=${token}`, { prefixUrl: '', responseType: 'json', }); const { size } = await prepareStream(stream); return { stream, size }; }); } async size({ id: meetingId, providerUserSession: { accessToken: token }, query, }) { return this.#withErrorHandling('provider.zoom.size.error', async () => { const client = getClient({ token }); const { recordingStart, recordingId: fileId } = query; const foundFile = await findFile({ client, meetingId, fileId, recordingStart, }); if (!foundFile) throw new Error('File not found'); return foundFile.file_size; // Note: May be undefined. }); } async logout({ companion, providerUserSession: { accessToken: token } }) { return this.#withErrorHandling('provider.zoom.logout.error', async () => { const { key, secret } = await companion.getProviderCredentials(); const { status } = await got .post('https://zoom.us/oauth/revoke', { searchParams: { token }, headers: { Authorization: getBasicAuthHeader(key, secret) }, responseType: 'json', }) .json(); return { revoked: status === 'success' }; }); } async deauthorizationCallback({ companion, body, headers }) { return this.#withErrorHandling('provider.zoom.deauth.error', async () => { if (!body || body.event !== DEAUTH_EVENT_NAME) { return { data: {}, status: 400 }; } const { verificationToken, key, secret } = await companion.getProviderCredentials(); const tokenSupplied = headers.authorization; if (!tokenSupplied || verificationToken !== tokenSupplied) { return { data: {}, status: 400 }; } await got.post('https://api.zoom.us/oauth/data/compliance', { headers: { Authorization: getBasicAuthHeader(key, secret) }, json: { client_id: key, user_id: body.payload.user_id, account_id: body.payload.account_id, deauthorization_event_received: body.payload, compliance_completed: true, }, responseType: 'json', }); return {}; }); } async #withErrorHandling(tag, fn) { const authErrorCodes = [ 124, // expired token 401, ]; return withProviderErrorHandling({ fn, tag, providerName: _a.oauthProvider, isAuthError: (response) => authErrorCodes.includes(response.statusCode), getJsonErrorMessage: (body) => body?.message, }); } } _a = Zoom; export default Zoom;