Selaa lähdekoodia

companion,zoom: add implementation for Zoom plugin and Zoom Provider (#2342)

* [WIP] First pass at adding zoom plugin and zoom companion service for allowing imports from zoom cloud

* WIP. Remove zoom example and move zoom companion code into core companion package, update list endpoint to retrieve based on pagination token or dates available, and get size / download endpoint working.

TO DO:
- fix logout / token expiry
- investigate why test for loading config from file is not working as expected
- confirm pagination works

* Modify provider views to allow for cases with zoom provider when insufficient results exist to enable infinite scroll (but users may need to retrieve older results), update types tests. Linters failing on the provider views files

* Update uppy and companion for the zoom integration to use folders to represent months, and retrieve 1 month at a time.

- clean up code around response data items to explicitly show fields, and match style of other provider methods
- update license date to 2020 across repo
- enable download/uploado of json timeline files
- fix companion test to load credentials from file
- remove front end code no longer required now that folders are being used instead of loading more with button

* Change initial check for user authentication to use same endpoint so we can reuse the same app permission list (ie dont require additional permissions to read user data)

* Clean up zoom provider index file, add api call back to retrieve user account information such as created timestamp and email, fix file icons, remove unused strings, remove license date updates from branch

* Add permission list to grant configurations, remove undefined response body check, update error to match style of other providers, reorder svg attribute to match other icons

* Add request to get user email address for when we are retrieving user meetings and meeting recordings within zoom provider, clean up logout api call

* Update adapter to use meeting UUID when returning the meeting id to match the meeting ID associated with the recording file meeting. This should also resolve issues around reused meeting IDs from zoom.

* Fix companion test

* uppy,robodog: do not add zoom yet

* zoom: add publishConfig

* test: remove uppy.Zoom use from typescript e2e test

Co-authored-by: Renée Kooi <renee@kooi.me>
mokutsu-coursera 4 vuotta sitten
vanhempi
commit
f1ef5bd809

+ 2 - 0
env.example.sh

@@ -13,6 +13,8 @@ export COMPANION_FACEBOOK_SECRET="***"
 export EDGLY_KEY="***"
 export EDGLY_SECRET="***"
 export GITHUB_TOKEN="***"
+export COMPANION_ZOOM_KEY="***"
+export COMPANION_ZOOM_SECRET="***"
 
 # Let's not set this by default, because that will make acceptance tests Always run on Saucelabs
 ## export SAUCE_ACCESS_KEY="***"

+ 2 - 0
examples/dev/Dashboard.js

@@ -5,6 +5,7 @@ const Facebook = require('@uppy/facebook/src')
 const OneDrive = require('@uppy/onedrive/src')
 const Dropbox = require('@uppy/dropbox/src')
 const GoogleDrive = require('@uppy/google-drive/src')
+const Zoom = require('@uppy/zoom/src')
 const Url = require('@uppy/url/src')
 const Webcam = require('@uppy/webcam/src')
 const ScreenCapture = require('@uppy/screen-capture/src')
@@ -67,6 +68,7 @@ module.exports = () => {
     .use(Dropbox, { target: Dashboard, companionUrl: COMPANION_URL })
     .use(Facebook, { target: Dashboard, companionUrl: COMPANION_URL })
     .use(OneDrive, { target: Dashboard, companionUrl: COMPANION_URL })
+    .use(Zoom, { target: Dashboard, companionUrl: COMPANION_URL })
     .use(Url, { target: Dashboard, companionUrl: COMPANION_URL })
     .use(Webcam, { target: Dashboard })
     .use(ScreenCapture, { target: Dashboard })

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 372 - 353
package-lock.json


+ 1 - 0
package.json

@@ -73,6 +73,7 @@
     "@uppy/webcam": "file:packages/@uppy/webcam",
     "@uppy/xhr-upload": "file:packages/@uppy/xhr-upload",
     "@uppy/image-editor": "file:packages/@uppy/image-editor",
+    "@uppy/zoom": "file:packages/@uppy/zoom",
     "remark-lint-uppy": "file:private/remark-lint-uppy",
     "uppy": "file:packages/uppy",
     "uppy.io": "file:website"

+ 3 - 0
packages/@uppy/companion/env.test.sh

@@ -15,3 +15,6 @@ export COMPANION_GOOGLE_SECRET="google_secret"
 
 export COMPANION_INSTAGRAM_KEY="instagram_key"
 export COMPANION_INSTAGRAM_SECRET="instagram_secret"
+
+export COMPANION_ZOOM_KEY="zoom_key"
+export COMPANION_ZOOM_SECRET="zoom_secret"

+ 4 - 0
packages/@uppy/companion/env_example

@@ -30,3 +30,7 @@ COMPANION_AWS_SECRET_FILE=
 COMPANION_AWS_BUCKET=
 COMPANION_AWS_ENDPOINT=
 COMPANION_AWS_REGION=
+
+COMPANION_ZOOM_KEY=
+COMPANION_ZOOM_SECRET=
+COMPANION_ZOOM_SECRET_FILE=

+ 7 - 0
packages/@uppy/companion/src/config/grant.js

@@ -29,6 +29,13 @@ module.exports = () => {
       transport: 'session',
       scope: ['files.read.all', 'offline_access', 'User.Read'],
       callback: '/onedrive/callback'
+    },
+    zoom: {
+      transport: 'session',
+      authorize_url: 'https://zoom.us/oauth/authorize',
+      access_url: 'https://zoom.us/oauth/token',
+      scope: ['recording:read', 'user:read'],
+      callback: '/zoom/callback'
     }
   }
 }

+ 27 - 1
packages/@uppy/companion/src/server/provider/index.js

@@ -9,11 +9,37 @@ const instagram = require('./instagram')
 const instagramGraph = require('./instagram/graph')
 const facebook = require('./facebook')
 const onedrive = require('./onedrive')
+const zoom = require('./zoom')
 const { getURLBuilder } = require('../helpers/utils')
 const logger = require('../logger')
 // eslint-disable-next-line
 const Provider = require('./Provider')
 
+// leave here for now until Purest Providers gets updated with Zoom provider
+config.zoom = {
+  'https://zoom.us/': {
+    __domain: {
+      auth: {
+        auth: { bearer: '[0]' }
+      }
+    },
+    '[version]/{endpoint}': {
+      __path: {
+        alias: '__default',
+        version: 'v2'
+      }
+    },
+    'oauth/revoke': {
+      __path: {
+        alias: 'logout',
+        auth: {
+          auth: { basic: '[0]' }
+        }
+      }
+    }
+  }
+}
+
 /**
  * adds the desired provider module to the request object,
  * based on the providerName parameter specified
@@ -47,7 +73,7 @@ module.exports.getProviderMiddleware = (providers) => {
 module.exports.getDefaultProviders = (companionOptions) => {
   const { providerOptions } = companionOptions || { providerOptions: null }
   // @todo: we should rename drive to googledrive or google-drive or google
-  const providers = { dropbox, drive, facebook, onedrive }
+  const providers = { dropbox, drive, facebook, onedrive, zoom }
   // Instagram's Graph API key is just numbers, while the old API key is hex
   const usesGraphAPI = () => /^\d+$/.test(providerOptions.instagram.key)
   if (providerOptions && providerOptions.instagram && usesGraphAPI()) {

+ 120 - 0
packages/@uppy/companion/src/server/provider/zoom/adapter.js

@@ -0,0 +1,120 @@
+const moment = require('moment')
+
+const MIMETYPES = {
+  MP4: 'video/mp4',
+  M4A: 'audio/mp4',
+  CHAT: 'text/plain',
+  TRANSCRIPT: 'text/vtt',
+  CC: 'text/vtt',
+  TIMELINE: 'application/json'
+}
+const ICONS = {
+  MP4: 'video',
+  M4A: 'file',
+  CHAT: 'file',
+  TRANSCRIPT: 'file',
+  CC: 'file',
+  FOLDER: 'folder',
+  TIMELINE: 'file'
+}
+
+exports.getDateName = (start, end) => {
+  return `${start.format('YYYY-MM-DD')} - ${end.format('YYYY-MM-DD')}`
+}
+
+exports.getAccountCreationDate = (results) => {
+  return moment(results.created_at)
+}
+
+exports.getUserEmail = (results) => {
+  return results.email
+}
+
+exports.getDateFolderId = (start, end) => {
+  return `${start.format('YYYY-MM-DD')}_${end.format('YYYY-MM-DD')}`
+}
+
+exports.getDateFolderRequestPath = (start, end) => {
+  return `?from=${start.format('YYYY-MM-DD')}&to=${end.format('YYYY-MM-DD')}`
+}
+
+exports.getDateFolderModified = (end) => {
+  return end.format('YYYY-MM-DD')
+}
+
+exports.getDateNextPagePath = (start) => {
+  return `?cursor=${start.subtract(1, 'days').format('YYYY-MM-DD')}`
+}
+
+exports.getNextPagePath = (results) => {
+  if (results.next_page_token) {
+    return `?cursor=${results.next_page_token}&from=${results.from}&to=${results.to}`
+  }
+  return null
+}
+// we rely on the file_type attribute to differentiate a recording file from other items
+exports.getIsFolder = (item) => {
+  return !item.file_type
+}
+
+exports.getItemName = (item) => {
+  const start = moment(item.start_time || item.recording_start)
+    .clone()
+    .format('YYYY-MM-DD, kk:mm')
+
+  if (item.file_type) {
+    const itemType = item.recording_type ? ` - ${item.recording_type.split('_').join(' ')}` : ''
+    return `${start}${itemType} (${item.file_type.toLowerCase()})`
+  }
+
+  return `${item.topic} (${start})`
+}
+
+exports.getIcon = (item) => {
+  if (item.file_type) {
+    return ICONS[item.file_type]
+  }
+  return ICONS.FOLDER
+}
+
+exports.getMimeType = (item) => {
+  if (item.file_type) {
+    return MIMETYPES[item.file_type]
+  }
+  return null
+}
+
+exports.getId = (item) => {
+  if (item.file_type && item.file_type === 'TIMELINE') {
+    return `${encodeURIComponent(item.meeting_id)}__TIMELINE`
+  } else if (item.file_type) {
+    return `${encodeURIComponent(item.meeting_id)}__${encodeURIComponent(item.id)}`
+  }
+  return `${encodeURIComponent(item.uuid)}`
+}
+
+exports.getRequestPath = (item) => {
+  if (item.file_type && item.file_type === 'TIMELINE') {
+    return `${encodeURIComponent(item.meeting_id)}?recordingId=TIMELINE`
+  } else if (item.file_type) {
+    return `${encodeURIComponent(item.meeting_id)}?recordingId=${encodeURIComponent(item.id)}`
+  }
+  return `${encodeURIComponent(item.uuid)}`
+}
+
+exports.getStartDate = (item) => {
+  if (item.file_type === 'TIMELINE') {
+    return item.recording_start
+  }
+  return item.start_time
+}
+
+exports.getSize = (item) => {
+  if (item.file_type && item.file_type === 'TIMELINE') {
+    const maxExportFileSize = 1024 * 1024
+    return maxExportFileSize
+  } else if (item.file_type) {
+    return item.file_size
+  }
+  return item.total_size
+}

+ 288 - 0
packages/@uppy/companion/src/server/provider/zoom/index.js

@@ -0,0 +1,288 @@
+const Provider = require('../Provider')
+
+const request = require('request')
+const moment = require('moment')
+const purest = require('purest')({ request })
+const logger = require('../../logger')
+const adapter = require('./adapter')
+const { ProviderApiError, ProviderAuthError } = require('../error')
+
+const BASE_URL = 'https://zoom.us/v2'
+const GET_LIST_PATH = '/users/me/recordings'
+const GET_USER_PATH = '/users/me'
+const PAGE_SIZE = 300
+const DEFAULT_RANGE_MOS = 23
+
+/**
+ * Adapter for API https://marketplace.zoom.us/docs/api-reference/zoom-api
+ */
+class Zoom extends Provider {
+  constructor (options) {
+    super(options)
+    this.authProvider = options.provider = Zoom.authProvider
+    this.client = purest(options)
+  }
+
+  static get authProvider () {
+    return 'zoom'
+  }
+
+  list (options, done) {
+    /*
+    - returns list of months by default
+    - drill down for specific files in each month
+    */
+    const token = options.token
+    const query = options.query || {}
+    const { cursor, from, to } = query
+    const meetingId = options.directory || ''
+    let meetingsPromise = Promise.resolve(undefined)
+    let recordingsPromise = Promise.resolve(undefined)
+
+    const userPromise = new Promise((resolve, reject) => {
+      this.client
+        .get(`${BASE_URL}${GET_USER_PATH}`)
+        .auth(token)
+        .request((err, resp, body) => {
+          if (err || resp.statusCode !== 200) {
+            return this._listError(err, resp, done)
+          }
+          resolve(resp)
+        })
+    })
+
+    if (from && to) {
+      const queryObj = {
+        page_size: PAGE_SIZE,
+        from,
+        to
+      }
+
+      if (cursor) {
+        queryObj.next_page_token = cursor
+      }
+
+      meetingsPromise = new Promise((resolve, reject) => {
+        this.client.get(`${BASE_URL}${GET_LIST_PATH}`)
+          .qs(queryObj)
+          .auth(token)
+          .request((err, resp, body) => {
+            if (err || resp.statusCode !== 200) {
+              return this._listError(err, resp, done)
+            } else {
+              resolve(resp)
+            }
+          })
+      })
+    } else if (meetingId) {
+      const GET_MEETING_FILES = `/meetings/${meetingId}/recordings`
+      recordingsPromise = new Promise((resolve, reject) => {
+        this.client
+          .get(`${BASE_URL}${GET_MEETING_FILES}`)
+          .auth(token)
+          .request((err, resp, body) => {
+            if (err || resp.statusCode !== 200) {
+              return this._listError(err, resp, done)
+            } else {
+              resolve(resp)
+            }
+          })
+      })
+    }
+
+    Promise.all([userPromise, meetingsPromise, recordingsPromise])
+      .then(
+        ([userResponse, meetingsResponse, recordingsResponse]) => {
+          let returnData = null
+          if (!meetingsResponse && !recordingsResponse) {
+            const end = cursor && moment(cursor)
+            returnData = this._initializeData(userResponse.body, end)
+          } else if (meetingsResponse) {
+            returnData = this._adaptData(userResponse.body, meetingsResponse.body)
+          } else if (recordingsResponse) {
+            returnData = this._adaptData(userResponse.body, recordingsResponse.body)
+          }
+          done(null, returnData)
+        },
+        (reqErr) => {
+          done(reqErr)
+        }
+      )
+  }
+
+  download ({ id, token, query }, done) {
+    // meeting id + file id required
+    // timeline files don't have an ID or size
+    const meetingId = id
+    const fileId = query.recordingId
+    const GET_MEETING_FILES = `/meetings/${meetingId}/recordings`
+
+    const downloadUrlPromise = new Promise((resolve) => {
+      this.client
+        .get(`${BASE_URL}${GET_MEETING_FILES}`)
+        .auth(token)
+        .request((err, resp) => {
+          if (err || resp.statusCode !== 200) {
+            return this._downloadError(resp, done)
+          }
+          const file = resp
+            .body
+            .recording_files
+            .find(file => fileId === file.id || fileId === file.file_type)
+          if (!file || !file.download_url) {
+            return this._downloadError(resp, done)
+          }
+          resolve(file.download_url)
+        })
+    })
+    downloadUrlPromise.then((downloadUrl) => {
+      this.client
+        .get(`${downloadUrl}?access_token=${token}`)
+        .request()
+        .on('response', (resp) => {
+          if (resp.statusCode !== 200) {
+            done(this._error(null, resp))
+          } else {
+            resp.on('data', (chunk) => done(null, chunk))
+          }
+        })
+        .on('end', () => {
+          done(null, null)
+        })
+        .on('error', (err) => {
+          logger.error(err, 'provider.zoom.download.error')
+          done(err)
+        })
+    })
+  }
+
+  size ({ id, token, query }, done) {
+    const meetingId = id
+    const fileId = query.recordingId
+    const GET_MEETING_FILES = `/meetings/${meetingId}/recordings`
+
+    return this.client
+      .get(`${BASE_URL}${GET_MEETING_FILES}`)
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          return this._downloadError(resp, done)
+        }
+        const file = resp
+          .body
+          .recording_files
+          .find(file => file.id === fileId || file.file_type === fileId)
+
+        if (!file) {
+          return this._downloadError(resp, done)
+        }
+        // timeline files don't have file size, but are typically small json files, should be much less than 1MB
+        const maxExportFileSize = 1024 * 1024
+        done(null, file.file_size || maxExportFileSize)
+      })
+  }
+
+  _initializeData (body, initialEnd = null) {
+    let end = initialEnd || moment()
+    let start = end.clone().date(1)
+
+    const accountCreation = adapter.getAccountCreationDate(body)
+    const defaultLimit = end.clone().subtract(DEFAULT_RANGE_MOS, 'months')
+    const limit = accountCreation > defaultLimit ? accountCreation : defaultLimit
+
+    const data = {
+      items: [],
+      username: adapter.getUserEmail(body)
+    }
+
+    while (start > limit) {
+      start = end.clone().date(1)
+      data.items.push({
+        isFolder: true,
+        icon: 'folder',
+        name: adapter.getDateName(start, end),
+        mimeType: null,
+        id: adapter.getDateFolderId(start, end),
+        thumbnail: null,
+        requestPath: adapter.getDateFolderRequestPath(start, end),
+        modifiedDate: adapter.getDateFolderModified(end),
+        size: null
+      })
+      end = start.clone().subtract(1, 'days')
+    }
+    data.nextPagePath = adapter.getDateNextPagePath(start)
+    return data
+  }
+
+  _adaptData (userResponse, results) {
+    if (!results) {
+      return { items: [] }
+    }
+
+    const data = {
+      nextPagePath: adapter.getNextPagePath(results),
+      items: [],
+      username: adapter.getUserEmail(userResponse)
+    }
+    const items = results.meetings || results.recording_files
+    items.forEach(item => {
+      data.items.push({
+        isFolder: adapter.getIsFolder(item),
+        icon: adapter.getIcon(item),
+        name: adapter.getItemName(item),
+        mimeType: adapter.getMimeType(item),
+        id: adapter.getId(item),
+        thumbnail: null,
+        requestPath: adapter.getRequestPath(item),
+        modifiedDate: adapter.getStartDate(item),
+        size: adapter.getSize(item)
+      })
+    })
+    return data
+  }
+
+  logout ({ token }, done) {
+    const encodedAuth = Buffer.from(
+      `${process.env.COMPANION_ZOOM_KEY}:${process.env.COMPANION_ZOOM_SECRET}`, 'binary'
+    ).toString('base64')
+    return this.client
+      .post('logout')
+      .auth(encodedAuth)
+      .qs({ token })
+      .request((err, resp) => {
+        if (err || resp.statusCode !== 200) {
+          logger.error(err, 'provider.zoom.logout.error')
+          done(this._error(err, resp))
+          return
+        }
+        done(null, { revoked: true })
+      })
+  }
+
+  _error (err, resp) {
+    const authErrorCodes = [
+      124, // expired token
+      401
+    ]
+    if (resp) {
+      const fallbackMsg = `request to ${this.authProvider} returned ${resp.statusCode}`
+      const errMsg = (resp.body || {}).message ? resp.body.message : fallbackMsg
+      return authErrorCodes.indexOf(resp.statusCode) > -1 ? new ProviderAuthError() : new ProviderApiError(errMsg, resp.statusCode)
+    }
+    return err
+  }
+
+  _downloadError (resp, done) {
+    const error = this._error(null, resp)
+    logger.error(error, 'provider.zoom.download.error')
+    return done(error)
+  }
+
+  _listError (err, resp, done) {
+    const error = this._error(err, resp)
+    logger.error(error, 'provider.zoom.list.error')
+    return done(error)
+  }
+}
+
+module.exports = Zoom

+ 4 - 0
packages/@uppy/companion/src/standalone/helper.js

@@ -49,6 +49,10 @@ const getConfigFromEnv = () => {
         key: process.env.COMPANION_ONEDRIVE_KEY,
         secret: getSecret('COMPANION_ONEDRIVE_SECRET')
       },
+      zoom: {
+        key: process.env.COMPANION_ZOOM_KEY,
+        secret: getSecret('COMPANION_ZOOM_SECRET')
+      },
       s3: {
         key: process.env.COMPANION_AWS_KEY,
         secret: getSecret('COMPANION_AWS_SECRET'),

+ 18 - 0
packages/@uppy/companion/test/__tests__/provider-manager.js

@@ -21,6 +21,8 @@ describe('Test Provider options', () => {
 
     expect(grantConfig.instagram.key).toBe('instagram_key')
     expect(grantConfig.instagram.secret).toBe('instagram_secret')
+    expect(grantConfig.zoom.key).toBe('zoom_key')
+    expect(grantConfig.zoom.secret).toBe('zoom_secret')
   })
 
   test('adds extra provider config', () => {
@@ -56,12 +58,23 @@ describe('Test Provider options', () => {
       ],
       callback: '/drive/callback'
     })
+    expect(grantConfig.zoom).toEqual({
+      key: 'zoom_key',
+      secret: 'zoom_secret',
+      transport: 'session',
+      authorize_url: 'https://zoom.us/oauth/authorize',
+      redirect_uri: 'http://localhost:3020/zoom/redirect',
+      access_url: 'https://zoom.us/oauth/token',
+      scope: ['recording:read', 'user:read'],
+      callback: '/zoom/callback'
+    })
   })
 
   test('adds provider options for secret files', () => {
     process.env.COMPANION_DROPBOX_SECRET_FILE = process.env.PWD + '/test/resources/dropbox_secret_file'
     process.env.COMPANION_GOOGLE_SECRET_FILE = process.env.PWD + '/test/resources/google_secret_file'
     process.env.COMPANION_INSTAGRAM_SECRET_FILE = process.env.PWD + '/test/resources/instagram_secret_file'
+    process.env.COMPANION_ZOOM_SECRET_FILE = process.env.PWD + '/test/resources/zoom_secret_file'
 
     companionOptions = getCompanionOptions()
 
@@ -70,6 +83,7 @@ describe('Test Provider options', () => {
     expect(grantConfig.dropbox.secret).toBe('xobpord')
     expect(grantConfig.google.secret).toBe('elgoog')
     expect(grantConfig.instagram.secret).toBe('margatsni')
+    expect(grantConfig.zoom.secret).toBe('u8Z5ceq')
   })
 
   test('does not add provider options if protocol and host are not set', () => {
@@ -85,6 +99,9 @@ describe('Test Provider options', () => {
 
     expect(grantConfig.instagram.key).toBeUndefined()
     expect(grantConfig.instagram.secret).toBeUndefined()
+
+    expect(grantConfig.zoom.key).toBeUndefined()
+    expect(grantConfig.zoom.secret).toBeUndefined()
   })
 
   test('sets a master redirect uri, if oauthDomain is set', () => {
@@ -94,6 +111,7 @@ describe('Test Provider options', () => {
     expect(grantConfig.dropbox.redirect_uri).toBe('http://domain.com/dropbox/redirect')
     expect(grantConfig.google.redirect_uri).toBe('http://domain.com/drive/redirect')
     expect(grantConfig.instagram.redirect_uri).toBe('http://domain.com/instagram/redirect')
+    expect(grantConfig.zoom.redirect_uri).toBe('http://domain.com/zoom/redirect')
   })
 })
 

+ 1 - 0
packages/@uppy/companion/test/resources/zoom_secret_file

@@ -0,0 +1 @@
+u8Z5ceq

+ 1 - 1
packages/@uppy/provider-views/src/Item/components/ItemIcon.js

@@ -18,7 +18,7 @@ function FolderIcon () {
 
 function VideoIcon () {
   return (
-    <svg aria-hidden="true" focusable="false" viewBox="0 0 58 58">
+    <svg aria-hidden="true" focusable="false" style={{ width: 16, marginRight: 4 }} viewBox="0 0 58 58">
       <path d="M36.537 28.156l-11-7a1.005 1.005 0 0 0-1.02-.033C24.2 21.3 24 21.635 24 22v14a1 1 0 0 0 1.537.844l11-7a1.002 1.002 0 0 0 0-1.688zM26 34.18V23.82L34.137 29 26 34.18z" />
       <path d="M57 6H1a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h56a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1zM10 28H2v-9h8v9zm-8 2h8v9H2v-9zm10 10V8h34v42H12V40zm44-12h-8v-9h8v9zm-8 2h8v9h-8v-9zm8-22v9h-8V8h8zM2 8h8v9H2V8zm0 42v-9h8v9H2zm54 0h-8v-9h8v9z" />
     </svg>

+ 1 - 0
packages/@uppy/transloadit/src/index.js

@@ -128,6 +128,7 @@ module.exports = class Transloadit extends Plugin {
     addPluginVersion('GoogleDrive', 'uppy-google-drive')
     addPluginVersion('Instagram', 'uppy-instagram')
     addPluginVersion('OneDrive', 'uppy-onedrive')
+    addPluginVersion('Zoom', 'uppy-zoom')
     addPluginVersion('Url', 'uppy-url')
 
     return list.join(',')

+ 21 - 0
packages/@uppy/zoom/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 Transloadit
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 40 - 0
packages/@uppy/zoom/README.md

@@ -0,0 +1,40 @@
+# @uppy/zoom
+
+<img src="https://uppy.io/images/logos/uppy-dog-head-arrow.svg" width="120" alt="Uppy logo: a superman puppy in a pink suit" align="right">
+
+<a href="https://www.npmjs.com/package/@uppy/zoom"><img src="https://img.shields.io/npm/v/@uppy/zoom.svg?style=flat-square"></a>
+<a href="https://travis-ci.org/transloadit/uppy"><img src="https://img.shields.io/travis/transloadit/uppy/master.svg?style=flat-square" alt="Build Status"></a>
+
+A description of this plugin or module goes here.
+
+Uppy is being developed by the folks at [Transloadit](https://transloadit.com), a versatile file encoding service.
+
+## Example
+
+```js
+const Uppy = require('@uppy/core')
+const Zoom = require('@uppy/zoom')
+
+const uppy = Uppy()
+uppy.use(Zoom, {
+  // Options
+})
+```
+
+## Installation
+
+```bash
+$ npm install @uppy/zoom --save
+```
+
+We recommend installing from npm and then using a module bundler such as [Webpack](https://webpack.js.org/), [Browserify](http://browserify.org/) or [Rollup.js](http://rollupjs.org/).
+
+Alternatively, you can also use this plugin in a pre-built bundle from Transloadit's CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object. See the [main Uppy documentation](https://uppy.io/docs/#Installation) for instructions.
+
+## Documentation
+
+Documentation for this plugin can be found on the [Uppy website](https://uppy.io/docs/zoom).
+
+## License
+
+[The MIT License](./LICENSE).

+ 34 - 0
packages/@uppy/zoom/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "@uppy/zoom",
+  "description": "Import files from zoom, into Uppy.",
+  "version": "0.0.1",
+  "license": "MIT",
+  "main": "lib/index.js",
+  "types": "types/index.d.ts",
+  "keywords": [
+    "file uploader",
+    "uppy",
+    "uppy-plugin",
+    "zoom"
+  ],
+  "homepage": "https://uppy.io",
+  "bugs": {
+    "url": "https://github.com/transloadit/uppy/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/transloadit/uppy.git"
+  },
+  "dependencies": {
+    "@uppy/companion-client": "file:../companion-client",
+    "@uppy/provider-views": "file:../provider-views",
+    "@uppy/utils": "file:../utils",
+    "preact": "8.2.9"
+  },
+  "peerDependencies": {
+    "@uppy/core": "^1.0.0"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}

+ 58 - 0
packages/@uppy/zoom/src/index.js

@@ -0,0 +1,58 @@
+const { Plugin } = require('@uppy/core')
+const { Provider } = require('@uppy/companion-client')
+const ProviderViews = require('@uppy/provider-views')
+const { h } = require('preact')
+
+module.exports = class Zoom extends Plugin {
+  static VERSION = require('../package.json').version
+
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = this.opts.id || 'Zoom'
+    Provider.initPlugin(this, opts)
+    this.title = this.opts.title || 'Zoom'
+    this.icon = () => (
+      <svg aria-hidden="true" focusable="false" width="32" height="32" viewBox="0 0 32 32">
+        <rect width="32" height="32" rx="16" fill="#0E71EB" />
+        <g fill="none" fill-rule="evenodd">
+          <path fill="#fff" d="M29,31H14c-1.657,0-3-1.343-3-3V17h15c1.657,0,3,1.343,3,3V31z" style="transform: translate(-5px, -5px) scale(0.9);" />
+          <polygon fill="#fff" points="37,31 31,27 31,21 37,17" style="transform: translate(-5px, -5px) scale(0.9);" />
+        </g>
+      </svg>
+    )
+
+    this.provider = new Provider(uppy, {
+      companionUrl: this.opts.companionUrl,
+      companionHeaders: this.opts.companionHeaders || this.opts.serverHeaders,
+      provider: 'zoom',
+      pluginId: this.id
+    })
+
+    this.onFirstRender = this.onFirstRender.bind(this)
+    this.render = this.render.bind(this)
+  }
+
+  install () {
+    this.view = new ProviderViews(this, {
+      provider: this.provider
+    })
+
+    const target = this.opts.target
+    if (target) {
+      this.mount(target, this)
+    }
+  }
+
+  uninstall () {
+    this.view.tearDown()
+    this.unmount()
+  }
+
+  onFirstRender () {
+    return this.view.getFolder()
+  }
+
+  render (state) {
+    return this.view.render(state)
+  }
+}

+ 17 - 0
packages/@uppy/zoom/types/index.d.ts

@@ -0,0 +1,17 @@
+import Uppy = require('@uppy/core')
+import CompanionClient = require('@uppy/companion-client')
+
+declare module Zoom {
+  interface ZoomOptions
+    extends Uppy.PluginOptions,
+      CompanionClient.PublicProviderOptions {
+    replaceTargetContent?: boolean
+    target?: Uppy.PluginTarget
+    title?: string
+    storage?: CompanionClient.TokenStorage
+  }
+}
+
+declare class Zoom extends Uppy.Plugin<Zoom.ZoomOptions> {}
+
+export = Zoom

+ 2 - 0
packages/@uppy/zoom/types/index.test-d.ts

@@ -0,0 +1,2 @@
+import Zoom = require('../')
+// TODO implement

+ 1 - 0
packages/uppy/types/index.test-d.ts

@@ -21,6 +21,7 @@ import * as Uppy from '../';
     .use(Uppy.Instagram, { target: Uppy.Dashboard, companionUrl: 'https://companion.uppy.io' })
     .use(Uppy.Webcam, { target: Uppy.Dashboard })
     .use(Uppy.ScreenCapture, { target: Uppy.Dashboard })
+    .use(Uppy.Zoom, { target: Uppy.Dashboard })
     .use(Uppy.Tus, { endpoint: 'https://master.tus.io/files/' })
     .on('complete', (result) => {
       console.log('Upload result:', result);

+ 69 - 0
test/endtoend/providers/provider.zoom.test.js

@@ -0,0 +1,69 @@
+/* global browser  */
+
+/*
+  WARNING! PLEASE READ THIS BEFORE ENABLING THIS TEST ON TRAVIS.
+
+  Before enabling this test on travis, please keep in mind that with this "no_ssl_bump_domains" option set
+  here https://github.com/transloadit/uppy/blob/998c7b1805acb8d305a562dd9726ebae98575e07/.travis.yml#L33
+  SSL encryption may not be enabled between the running companion and the testing browser client.
+
+  Hence, provider tokens (Google, Instagram, Zoom) may be at risk of getting hijacked.
+*/
+const { finishUploadTest, startUploadTest, uploadWithRetry } = require('./helper')
+const testURL = 'http://localhost:4567/providers'
+
+describe('File upload with Zoom Provider', () => {
+  beforeEach(async () => {
+    await browser.url(testURL)
+  })
+
+  // not using arrow functions as cb so to keep mocha in the 'this' context
+  it('should upload a file completely with Zoom', async function () {
+    if (!process.env.UPPY_GOOGLE_EMAIL) {
+      console.log('skipping Zoom integration test')
+      return this.skip()
+    }
+
+    // ensure session is cleared
+    await startUploadTest(browser, 'Zoom', /zoom/)
+    await signIntoGoogle(browser)
+    await finishUploadTest(browser)
+  })
+
+  // not using arrow functions as cb so to keep mocha in the 'this' context
+  it('should resume uploads when retry is triggered with Zoom', async function () {
+    if (!process.env.UPPY_GOOGLE_EMAIL) {
+      console.log('skipping Zoom integration test')
+      return this.skip()
+    }
+
+    await uploadWithRetry(browser, 'Zoom', testURL)
+  })
+})
+
+const signIntoGoogle = async (browser) => {
+  const emailInput = await browser.$('#identifierId')
+  await emailInput.waitForExist(30000)
+  await emailInput.setValue(process.env.UPPY_GOOGLE_EMAIL)
+  let nextButton = await browser.$('#identifierNext')
+  await nextButton.click()
+
+  const passwordInput = await browser.$('input[name=password]')
+  await passwordInput.waitForDisplayed(30000)
+  await passwordInput.setValue(process.env.UPPY_GOOGLE_PASSWORD)
+  nextButton = await browser.$('#passwordNext')
+  await nextButton.click()
+  await browser.pause(3000)
+
+  const emailListItem = await browser.$(`li div[data-identifier="${process.env.UPPY_GOOGLE_EMAIL}"]`)
+  if (await emailListItem.isExisting()) {
+    // if user is already signed in, just select user
+    await emailListItem.click()
+  }
+
+  const allowZoomButton = await browser.$('#submit_approve_access')
+  if (await allowZoomButton.isExisting()) {
+    // if Zoom has never been allowed, allow it
+    await allowZoomButton.click()
+  }
+}

+ 6 - 0
website/src/docs/companion.md

@@ -190,6 +190,12 @@ export COMPANION_ONEDRIVE_SECRET="YOUR ONEDRIVE SECRET"
 # specifying a secret file will override a directly set secret
 export COMPANION_ONEDRIVE_SECRET_FILE="PATH/TO/ONEDRIVE/SECRET/FILE"
 
+# to enable Zoom
+export COMPANION_ZOOM_KEY="YOUR ZOOM KEY"
+export COMPANION_ZOOM_SECRET="YOUR ZOOM SECRET"
+# specifying a secret file will override a directly set secret
+export COMPANION_ZOOM_SECRET_FILE="PATH/TO/ZOOM/SECRET/FILE"
+
 # to enable S3
 export COMPANION_AWS_KEY="YOUR AWS KEY"
 export COMPANION_AWS_SECRET="YOUR AWS SECRET"

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä