cal.pub0.org/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts

313 lines
10 KiB
TypeScript
Raw Normal View History

import { Credential } from "@prisma/client";
2022-06-11 22:15:29 +00:00
import { z } from "zod";
2022-06-11 22:15:29 +00:00
import { handleErrorsJson } from "@calcom/lib/errors";
App Store (#1869) * patch applied * patch applied * We shouldn't pollute global css * Build fixes * Updates typings * WIP extracting zoom to package * Revert "Upgrades next to 12.1 (#1895)" (#1903) This reverts commit ede0e98e1f7d462fe7196c6ce0de29490c00331e. * Tweak/gitignore prisma zod (#1905) * Extracts ignored createEventTypeBaseInput * Adds postinstall script * Revert "Tweak/gitignore prisma zod (#1905)" (#1906) This reverts commit 15bfeb30d7ce22a44f6dce9a74803a97ef43e2e6. * Eslint fixes (#1898) * Eslint fixes * Docs build fixes * Upgrade to next 12.1 (#1904) * Upgrades next to 12.1 * Fixes build * Updaters e2e test pipelines Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Fix URL by removing slash and backslash (#1733) * Fix URl by removing slash and backslash * Implement slugify * Add data type * Fixing folder structure * Solve zod-utils conflict * Build fixes (#1929) * Build fixes * Fixes type error * WIP * Conflict fixes * Removes unused file * TODO * WIP * Type fixes * Linting * WIP * Moved App definition to types * WIP * WIP * WIP * WIP WIP * Renamed zoomvideo app * Import fix * Daily.co app (#2022) * Daily.co app * Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts Co-authored-by: Omar López <zomars@me.com> * Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts Co-authored-by: Omar López <zomars@me.com> * Missing deps for newly added contants to lib Co-authored-by: Omar López <zomars@me.com> * WIP * WIP * WIP * Daily fixes * Updated type info * Slack Oauth integration - api route ideas * Adds getLocationOptions * Type fixes * Adds location option for daily video * Revert "Slack Oauth integration - api route ideas" This reverts commit 35ffa78e929339c4badb98cdab4e4b953ecc7cca. * Slack Oauth + verify sig * Revert "Slack Oauth + verify sig" This reverts commit ee95795e0f0ae6d06be4e0a423afb8c315d9af7d. * Huddle01 migration to app store (#2038) * Jitsi Video App migration * Removing uneeded dependencies * Missed unused reference * Missing dependency `@calcom/lib` is needed in the `locationOption.ts` file * Huddle01 migration to app store * Jitsi Video App migration (#2027) * Jitsi Video App migration * Removing uneeded dependencies * Missed unused reference * Missing dependency `@calcom/lib` is needed in the `locationOption.ts` file Co-authored-by: Omar López <zomars@me.com> * Monorepo/app store MS Teams Integration (#2080) * Create teamsvideo package * Remove zoom specific refrences * Add teams video files * Rename to office365_video * Add call back to add crednetial type office365_teams * Rename to office_video to match type * Add MS Teams as a location option * Rename files * Add teams reponse interface and create meeting * Comment out Daily imports * Add check for Teams integration * Add token checking functions * Change template to create event rather than meeting * Add comment to test between create link and event * Add teams URL to booking * Ask for just onlineMeeting permission * Add MS Teams logo * Add message to have an enterprise account * Remove comments * Comment back hasDailyIntegration * Comment back daily credentials * Update link to MS Graph section of README * Move API calls to package Co-authored-by: Omar López <zomars@me.com> * Re-adds missing module for transpiling * Adds email as required field for app store metadata * WIP: migrates tandem to app store * Cleanup * Migrates tandem api routes to app store * Fixes tandem api handlers * Big WIP WIP * Build fixes * WIP * Fixes annoying circular dependency bug I've spent a whole day on this.... * Location option cleanup * Type fixes * Update EventManager.ts * Update CalendarManager.ts * Moves CalendarService back to lib * Moves apple calendar to App Store * Cleanup * More cleanup * Migrates apple calendar * Returns all connected calendars credentials * No tsx needed in calcom/lib * Update auth.ts * Reordering * Update i18n.utils.ts * WIP: Google Meet * Type fixes * Type fixes * Cleanup * Update LinkIconButton.tsx * Update TrialBanner.tsx * Cleanup * Cleanup * Type fixes * Update _appRegistry.ts * Update fonts.css * Update CalEventParser.ts * Delete yarn.lock.rej * Update eslint-preset.js * Delete zoom.tsx * Type fixes * Migrates caldav to app store * Cleanup * Type fixes * Adds caldav to app store * Test fixes * Updates integration tests * Moar test fixes * Redirection fixes * Redirection fixes * Update timeFormat.ts * Update booking-pages.test.ts * Connect button fixes * Fix empty item * Cal fixes andrea (#2234) * Fixes #2178 * Fixes #2178 * Update apps/web/components/availability/Schedule.tsx * Update apps/web/components/availability/Schedule.tsx Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Peer Richelsen <peer@cal.com> * added meta viewport to disable zoom on input focus on mobile (#2238) * Update lint.yml (#2211) Co-authored-by: Peer Richelsen <peeroke@gmail.com> * Fix prisma client bundle makes app slow (#2237) Co-authored-by: Omar López <zomars@me.com> * Slider fixes * Removed unused code * Full Shell when unauthed * App sidebar responsive fixes * Adds dynamic install button * Fix for duplicate connected calendars * Various fixes * Display notification on app delete * Reuse connect button * Adds CalDav button * Deprecates ConnectIntegration * Simplify install button * Adds Google Calendar connect button * Adds Office 365 Install button * Migrates Stripe to App Store * Zoom Install Button (#2244) * Fix minor css, app image load from static path * Fix app logos remote img src (#2252) * Adds missing exports * Cleanup * Disables install button for globally enabled apps * Update EventManager.ts * Stripe fixes * Disables example app Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Juan Esteban Nieto Cifuentes <89233604+Jenietoc@users.noreply.github.com> Co-authored-by: Leo Giovanetti <hello@leog.me> Co-authored-by: Sean Brydon <seanbrydon.me@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: andreaestefania12 <andreaestefania12@hotmail.com> Co-authored-by: Peer Richelsen <peer@cal.com> Co-authored-by: Demian Caldelas <denik.works@protonmail.com> Co-authored-by: Alan <alannnc@gmail.com>
2022-03-23 22:00:30 +00:00
import prisma from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { PartialReference } from "@calcom/types/EventManager";
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
2022-06-11 22:15:29 +00:00
import { getZoomAppKeys } from "./getZoomAppKeys";
2022-05-02 23:13:34 +00:00
/** @link https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate */
export const zoomEventResultSchema = z
.object({
assistant_id: z.string(),
host_email: z.string(),
id: z.number(),
registration_url: z.string(),
agenda: z.string(),
created_at: z.string(),
duration: z.number(),
h323_password: z.string(),
join_url: z.string(),
occurrences: z.array(
z.object({
duration: z.number(),
occurrence_id: z.string(),
start_time: z.string(),
status: z.string(),
})
),
password: z.string(),
pmi: z.number(),
pre_schedule: z.boolean(),
recurrence: z.object({
end_date_time: z.string(),
end_times: z.number(),
monthly_day: z.number(),
monthly_week: z.number(),
monthly_week_day: z.number(),
repeat_interval: z.number(),
type: z.number(),
weekly_days: z.string(),
2022-06-11 22:15:29 +00:00
}),
settings: z.object({
allow_multiple_devices: z.boolean(),
alternative_hosts: z.string(),
alternative_hosts_email_notification: z.boolean(),
alternative_host_update_polls: z.boolean(),
approval_type: z.number(),
approved_or_denied_countries_or_regions: z.object({
approved_list: z.array(z.string()),
denied_list: z.array(z.string()),
enable: z.boolean(),
method: z.string(),
}),
audio: z.string(),
authentication_domains: z.string(),
authentication_exception: z.array(
z.object({ email: z.string(), name: z.string(), join_url: z.string() })
),
authentication_name: z.string(),
authentication_option: z.string(),
auto_recording: z.string(),
breakout_room: z.object({
enable: z.boolean(),
rooms: z.array(z.object({ name: z.string(), participants: z.array(z.string()) })),
}),
calendar_type: z.number(),
close_registration: z.boolean(),
cn_meeting: z.boolean(),
contact_email: z.string(),
contact_name: z.string(),
custom_keys: z.array(z.object({ key: z.string(), value: z.string() })),
email_notification: z.boolean(),
encryption_type: z.string(),
enforce_login: z.boolean(),
enforce_login_domains: z.string(),
focus_mode: z.boolean(),
global_dial_in_countries: z.array(z.string()),
global_dial_in_numbers: z.array(
z.object({
city: z.string(),
country: z.string(),
country_name: z.string(),
number: z.string(),
type: z.string(),
})
),
host_video: z.boolean(),
in_meeting: z.boolean(),
jbh_time: z.number(),
join_before_host: z.boolean(),
language_interpretation: z.object({
enable: z.boolean(),
interpreters: z.array(z.object({ email: z.string(), languages: z.string() })),
}),
meeting_authentication: z.boolean(),
mute_upon_entry: z.boolean(),
participant_video: z.boolean(),
private_meeting: z.boolean(),
registrants_confirmation_email: z.boolean(),
registrants_email_notification: z.boolean(),
registration_type: z.number(),
show_share_button: z.boolean(),
use_pmi: z.boolean(),
waiting_room: z.boolean(),
watermark: z.boolean(),
host_save_video_order: z.boolean(),
}),
start_time: z.string(),
start_url: z.string(),
timezone: z.string(),
topic: z.string(),
tracking_fields: z.array(z.object({ field: z.string(), value: z.string(), visible: z.boolean() })),
type: z.number(),
})
.deepPartial();
2022-06-11 22:15:29 +00:00
export type ZoomEventResult = z.infer<typeof zoomEventResultSchema>;
export const zoomMeetingsSchema = z.object({
next_page_token: z.string(),
page_count: z.number(),
page_number: z.number(),
page_size: z.number(),
total_records: z.number(),
meetings: z.array(
z.object({
agenda: z.string(),
created_at: z.string(),
duration: z.number(),
host_id: z.string(),
id: z.number(),
join_url: z.string(),
pmi: z.string(),
start_time: z.string(),
timezone: z.string(),
topic: z.string(),
type: z.number(),
uuid: z.string(),
})
),
});
const zoomTokenSchema = z.object({
scope: z.string().regex(new RegExp("meeting:write")),
2022-06-11 22:15:29 +00:00
expiry_date: z.number(),
expires_in: z.number().optional(), // deprecated, purely for backwards compatibility; superseeded by expiry_date.
token_type: z.literal("bearer"),
access_token: z.string(),
refresh_token: z.string(),
});
type ZoomToken = z.infer<typeof zoomTokenSchema>;
const zoomAuth = (credential: Credential) => {
const credentialKey = zoomTokenSchema.parse(credential.key);
const isTokenValid = (token: ZoomToken) =>
token && token.token_type && token.access_token && (token.expires_in || token.expiry_date) < Date.now();
2022-06-11 22:15:29 +00:00
const refreshAccessToken = async (refreshToken: string) => {
const { client_id, client_secret } = await getZoomAppKeys();
const authHeader = "Basic " + Buffer.from(client_id + ":" + client_secret).toString("base64");
return fetch("https://zoom.us/oauth/token", {
method: "POST",
headers: {
Authorization: authHeader,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
refresh_token: refreshToken,
grant_type: "refresh_token",
}),
})
.then(handleErrorsJson)
.then(async (responseBody) => {
// set expiry date as offset from current time.
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
delete responseBody.expires_in;
// Store new tokens in database.
await prisma.credential.update({
where: {
id: credential.id,
},
data: {
key: responseBody,
},
});
credentialKey.expiry_date = responseBody.expiry_date;
credentialKey.access_token = responseBody.access_token;
return credentialKey.access_token;
});
2022-06-11 22:15:29 +00:00
};
return {
getToken: () =>
!isTokenValid(credentialKey)
? Promise.resolve(credentialKey.access_token)
: refreshAccessToken(credentialKey.refresh_token),
};
};
const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
const translateEvent = (event: CalendarEvent) => {
// Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
return {
topic: event.title,
type: 2, // Means that this is a scheduled meeting
start_time: event.startTime,
duration: (new Date(event.endTime).getTime() - new Date(event.startTime).getTime()) / 60000,
//schedule_for: "string", TODO: Used when scheduling the meeting for someone else (needed?)
timezone: event.attendees[0].timeZone,
//password: "string", TODO: Should we use a password? Maybe generate a random one?
agenda: event.description,
settings: {
host_video: true,
participant_video: true,
cn_meeting: false, // TODO: true if host meeting in China
in_meeting: false, // TODO: true if host meeting in India
join_before_host: true,
mute_upon_entry: false,
watermark: false,
use_pmi: false,
approval_type: 2,
audio: "both",
auto_recording: "none",
enforce_login: false,
registrants_email_notification: true,
},
};
};
2022-06-11 22:15:29 +00:00
const fetchZoomApi = async (endpoint: string, options?: RequestInit) => {
const auth = zoomAuth(credential);
const accessToken = await auth.getToken();
const responseBody = await fetch(`https://api.zoom.us/v2/${endpoint}`, {
method: "GET",
...options,
headers: {
Authorization: "Bearer " + accessToken,
...options?.headers,
},
}).then(handleErrorsJson);
return responseBody;
};
return {
2022-05-03 00:21:09 +00:00
getAvailability: async () => {
2022-06-11 22:15:29 +00:00
try {
// TODO Possibly implement pagination for cases when there are more than 300 meetings already scheduled.
const responseBody = await fetchZoomApi("users/me/meetings?type=scheduled&page_size=300");
const data = zoomMeetingsSchema.parse(responseBody);
return data.meetings.map((meeting) => ({
start: meeting.start_time,
end: new Date(new Date(meeting.start_time).getTime() + meeting.duration * 60000).toISOString(),
}));
} catch (err) {
console.error(err);
/* Prevents booking failure when Zoom Token is expired */
return [];
}
},
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> => {
2022-06-11 22:15:29 +00:00
const response: ZoomEventResult = await fetchZoomApi("users/me/meetings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(translateEvent(event)),
2022-06-11 22:15:29 +00:00
});
const result = zoomEventResultSchema.passthrough().parse(response);
if (result.id && result.join_url) {
return Promise.resolve({
type: "zoom_video",
id: result.id.toString(),
password: result.password || "",
url: result.join_url,
});
}
return Promise.reject(new Error("Failed to create meeting"));
},
deleteMeeting: async (uid: string): Promise<void> => {
2022-06-11 22:15:29 +00:00
await fetchZoomApi(`meetings/${uid}`, {
method: "DELETE",
2022-06-11 22:15:29 +00:00
});
return Promise.resolve();
},
updateMeeting: async (bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> => {
2022-06-11 22:15:29 +00:00
await fetchZoomApi(`meetings/${bookingRef.uid}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(translateEvent(event)),
2022-06-11 22:15:29 +00:00
});
return Promise.resolve({
type: "zoom_video",
id: bookingRef.meetingId as string,
password: bookingRef.meetingPassword as string,
url: bookingRef.meetingUrl as string,
});
},
};
};
export default ZoomVideoApiAdapter;