index.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. const moment = require('moment-timezone')
  2. const Provider = require('../Provider')
  3. const { initializeData, adaptData } = require('./adapter')
  4. const { withProviderErrorHandling } = require('../providerErrors')
  5. const { prepareStream, getBasicAuthHeader } = require('../../helpers/utils')
  6. const got = require('../../got')
  7. const BASE_URL = 'https://zoom.us/v2'
  8. const PAGE_SIZE = 300
  9. const DEAUTH_EVENT_NAME = 'app_deauthorized'
  10. const getClient = async ({ token }) => (await got).extend({
  11. prefixUrl: BASE_URL,
  12. headers: {
  13. authorization: `Bearer ${token}`,
  14. },
  15. })
  16. async function findFile ({ client, meetingId, fileId, recordingStart }) {
  17. const { recording_files: files } = await client.get(`meetings/${encodeURIComponent(meetingId)}/recordings`, { responseType: 'json' }).json()
  18. return files.find((file) => (
  19. fileId === file.id || (file.file_type === fileId && file.recording_start === recordingStart)
  20. ))
  21. }
  22. /**
  23. * Adapter for API https://marketplace.zoom.us/docs/api-reference/zoom-api
  24. */
  25. class Zoom extends Provider {
  26. static get authProvider () {
  27. return 'zoom'
  28. }
  29. /*
  30. - returns list of months by default
  31. - drill down for specific files in each month
  32. */
  33. async list (options) {
  34. return this.#withErrorHandling('provider.zoom.list.error', async () => {
  35. const { token } = options
  36. const query = options.query || {}
  37. const { cursor, from, to } = query
  38. const meetingId = options.directory || ''
  39. const client = await getClient({ token })
  40. const user = await client.get('users/me', { responseType: 'json' }).json()
  41. const { timezone } = user
  42. if (!from && !to && !meetingId) {
  43. const end = cursor && moment.utc(cursor).endOf('day').tz(timezone || 'UTC')
  44. return initializeData(user, end)
  45. }
  46. if (from && to) {
  47. /* we need to convert local datetime to UTC date for Zoom query
  48. eg: user in PST (UTC-08:00) wants 2020-08-01 (00:00) to 2020-08-31 (23:59)
  49. => in UTC, that's 2020-07-31 (16:00) to 2020-08-31 (15:59)
  50. */
  51. const searchParams = {
  52. page_size: PAGE_SIZE,
  53. from: moment.tz(from, timezone || 'UTC').startOf('day').tz('UTC').format('YYYY-MM-DD'),
  54. to: moment.tz(to, timezone || 'UTC').endOf('day').tz('UTC').format('YYYY-MM-DD'),
  55. }
  56. if (cursor) searchParams.next_page_token = cursor
  57. const meetingsInfo = await client.get('users/me/recordings', { searchParams, responseType: 'json' }).json()
  58. return adaptData(user, meetingsInfo, query)
  59. }
  60. if (meetingId) {
  61. const recordingInfo = await client.get(`meetings/${encodeURIComponent(meetingId)}/recordings`, { responseType: 'json' }).json()
  62. return adaptData(user, recordingInfo, query)
  63. }
  64. throw new Error('Invalid list() arguments')
  65. })
  66. }
  67. async download ({ id: meetingId, token, query }) {
  68. return this.#withErrorHandling('provider.zoom.download.error', async () => {
  69. // meeting id + file id required
  70. // cc files don't have an ID or size
  71. const { recordingStart, recordingId: fileId } = query
  72. const client = await getClient({ token })
  73. const foundFile = await findFile({ client, meetingId, fileId, recordingStart })
  74. const url = foundFile?.download_url
  75. if (!url) throw new Error('Download URL not found')
  76. const stream = client.stream.get(`${url}?access_token=${token}`, { prefixUrl: '', responseType: 'json' })
  77. await prepareStream(stream)
  78. return { stream }
  79. })
  80. }
  81. async size ({ id: meetingId, token, query }) {
  82. return this.#withErrorHandling('provider.zoom.size.error', async () => {
  83. const client = await getClient({ token })
  84. const { recordingStart, recordingId: fileId } = query
  85. const foundFile = await findFile({ client, meetingId, fileId, recordingStart })
  86. if (!foundFile) throw new Error('File not found')
  87. return foundFile.file_size // Note: May be undefined.
  88. })
  89. }
  90. async logout ({ companion, token }) {
  91. return this.#withErrorHandling('provider.zoom.logout.error', async () => {
  92. const { key, secret } = await companion.getProviderCredentials()
  93. const { status } = await (await got).post('https://zoom.us/oauth/revoke', {
  94. searchParams: { token },
  95. headers: { Authorization: getBasicAuthHeader(key, secret) },
  96. responseType: 'json',
  97. }).json()
  98. return { revoked: status === 'success' }
  99. })
  100. }
  101. async deauthorizationCallback ({ companion, body, headers }) {
  102. return this.#withErrorHandling('provider.zoom.deauth.error', async () => {
  103. if (!body || body.event !== DEAUTH_EVENT_NAME) {
  104. return { data: {}, status: 400 }
  105. }
  106. const { verificationToken, key, secret } = await companion.getProviderCredentials()
  107. const tokenSupplied = headers.authorization
  108. if (!tokenSupplied || verificationToken !== tokenSupplied) {
  109. return { data: {}, status: 400 }
  110. }
  111. await (await got).post('https://api.zoom.us/oauth/data/compliance', {
  112. headers: { Authorization: getBasicAuthHeader(key, secret) },
  113. json: {
  114. client_id: key,
  115. user_id: body.payload.user_id,
  116. account_id: body.payload.account_id,
  117. deauthorization_event_received: body.payload,
  118. compliance_completed: true,
  119. },
  120. responseType: 'json',
  121. })
  122. return {}
  123. })
  124. }
  125. // eslint-disable-next-line class-methods-use-this
  126. async #withErrorHandling (tag, fn) {
  127. const authErrorCodes = [
  128. 124, // expired token
  129. 401,
  130. ]
  131. return withProviderErrorHandling({
  132. fn,
  133. tag,
  134. providerName: Zoom.authProvider,
  135. isAuthError: (response) => authErrorCodes.includes(response.statusCode),
  136. getJsonErrorMessage: (body) => body?.message,
  137. })
  138. }
  139. }
  140. module.exports = Zoom