Fix/fallback to calvideo (#4992)

* try/catch on createMeeting zoom

* add handle response body from zoom

* db invalid credential flag

* Update VideoApiAdapter.ts

* Added notes and try/catches

* Remove double response error validation

* Fallback to calvideo

* Fix types for credentials

* Add nullable to invalid field from credential table

* Fix calendar service credential creation with bad type

* revert db migration invalid on credential table

* Revert "Fix types for credentials"

This reverts commit 1623305aea23a33c39ba7b6fb5604dd71c6205d7.

* Revert "Fix calendar service credential creation with bad type"

This reverts commit 3fd3b08f2ee6c7019428a8ff454d371e23759ad7.

* Remove migration

* Fix for property name for api key on dailyvideo

* remove unused prop from credential

* Revert "Fix for property name for api key on dailyvideo"

This reverts commit 32976934e05e6923210718ccd5634c1357828f65.

* Tests fixes

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
alannnc
2022-10-14 02:36:43 +00:00
committed by GitHub
co-authored by Peer Richelsen zomars
parent 74a16e28f4
commit 1c165bd934
2 changed files with 99 additions and 52 deletions
@@ -20,6 +20,7 @@ const zoomEventResultSchema = z.object({
export type ZoomEventResult = z.infer<typeof zoomEventResultSchema>;
// @TODO: add link to the docs
export const zoomMeetingsSchema = z.object({
next_page_token: z.string(),
page_count: z.number(),
@@ -45,6 +46,7 @@ export const zoomMeetingsSchema = z.object({
});
// Successful API response
// @TODO: add link to the docs
const zoomTokenSchema = z.object({
scope: z.string().regex(new RegExp("meeting:write")),
expiry_date: z.number(),
@@ -58,11 +60,21 @@ type ZoomToken = z.infer<typeof zoomTokenSchema>;
const isTokenValid = (token: ZoomToken) => (token.expires_in || token.expiry_date) < Date.now();
/** @link https://marketplace.zoom.us/docs/guides/auth/oauth/#request */
const zoomRefreshedTokenSchema = z.object({
access_token: z.string(),
token_type: z.literal("bearer"),
refresh_token: z.string(),
expires_in: z.number(),
scope: z.string(),
});
const zoomAuth = (credential: Credential) => {
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", {
const response = await fetch("https://zoom.us/oauth/token", {
method: "POST",
headers: {
Authorization: authHeader,
@@ -72,24 +84,32 @@ const zoomAuth = (credential: Credential) => {
refresh_token: refreshToken,
grant_type: "refresh_token",
}),
})
.then(handleErrorsJson)
.then(async (responseBody) => {
if (!responseBody.refresh_token) Promise.reject(new Error("Invalid credentials"));
// 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,
},
});
return responseBody.access_token;
});
});
const responseBody = await handleErrorsJson(response);
// We check the if the new credentials matches the expected response structure
const parsedToken = zoomRefreshedTokenSchema.safeParse(responseBody);
// TODO: If the new token is invalid, initiate the fallback sequence instead of throwing
// Expanding on this we can use server-to-server app and create meeting from admin calcom account
if (!parsedToken.success) {
return Promise.reject(new Error("Invalid refreshed tokens were returned"));
}
const newTokens = parsedToken.data;
const oldCredential = await prisma.credential.findUniqueOrThrow({ where: { id: credential.id } });
const parsedKey = zoomTokenSchema.safeParse(oldCredential.key);
if (!parsedKey.success) {
return Promise.reject(new Error("Invalid credentials were saved in the DB"));
}
const key = parsedKey.data;
key.access_token = newTokens.access_token;
key.refresh_token = newTokens.refresh_token;
// set expiry date as offset from current time.
key.expiry_date = Math.round(Date.now() + newTokens.expires_in * 1000);
// Store new tokens in database.
await prisma.credential.update({ where: { id: credential.id }, data: { key } });
return newTokens.access_token;
};
return {
@@ -98,12 +118,7 @@ const zoomAuth = (credential: Credential) => {
try {
credentialKey = zoomTokenSchema.parse(credential.key);
} catch (error) {
// If the parse fails, it means we have an invalid credential saved. We should delete it.
await prisma.credential.delete({ where: { id: credential.id } });
// We reject the promise, and should handle the error on the UI
return Promise.reject(
"This Zoom credential was malformed so we've removed it from the DB. Please add a new Zoom credential"
);
return Promise.reject("Zoom credential keys parsing error");
}
return !isTokenValid(credentialKey)
@@ -237,24 +252,31 @@ const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
}
},
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> => {
const response: ZoomEventResult = await fetchZoomApi("users/me/meetings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(translateEvent(event)),
});
const result = zoomEventResultSchema.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,
try {
const response: ZoomEventResult = await fetchZoomApi("users/me/meetings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(translateEvent(event)),
});
const result = zoomEventResultSchema.parse(response);
if (result.id && result.join_url) {
return {
type: "zoom_video",
id: result.id.toString(),
password: result.password || "",
url: result.join_url,
};
}
throw new Error("Failed to create meeting. Response is " + JSON.stringify(result));
} catch (err) {
console.error(err);
/* Prevents meeting creation failure when Zoom Token is expired */
throw new Error("Unexpected error");
}
return Promise.reject(new Error("Failed to create meeting. Response is " + JSON.stringify(result)));
},
deleteMeeting: async (uid: string): Promise<void> => {
await fetchZoomApi(`meetings/${uid}`, {
+36 -11
View File
@@ -3,6 +3,7 @@ import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import appStore from "@calcom/app-store";
import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys";
import { sendBrokenIntegrationEmail } from "@calcom/emails";
import { getUid } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
@@ -44,18 +45,28 @@ const createMeeting = async (credential: Credential, calEvent: CalendarEvent) =>
const videoAdapters = getVideoAdapters([credential]);
const [firstVideoAdapter] = videoAdapters;
const createdMeeting = await firstVideoAdapter?.createMeeting(calEvent).catch(async (e) => {
await sendBrokenIntegrationEmail(calEvent, "video");
console.error("createMeeting failed", e, calEvent);
});
let createdMeeting;
try {
createdMeeting = await firstVideoAdapter?.createMeeting(calEvent);
if (!createdMeeting) {
return {
type: credential.type,
success: false,
uid,
originalEvent: calEvent,
};
if (!createdMeeting) {
return {
type: credential.type,
success: false,
uid,
originalEvent: calEvent,
};
}
} catch (err) {
await sendBrokenIntegrationEmail(calEvent, "video");
console.error("createMeeting failed", err, calEvent);
// Default to calVideo
const defaultMeeting = await createMeetingWithCalVideo(calEvent);
if (defaultMeeting) {
createdMeeting = defaultMeeting;
calEvent.location = "integrations:dailyvideo";
}
}
return {
@@ -117,4 +128,18 @@ const deleteMeeting = (credential: Credential, uid: string): Promise<unknown> =>
return Promise.resolve({});
};
// @TODO: This is a temporary solution to create a meeting with cal.com video as fallback url
const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => {
const [videoAdapter] = getVideoAdapters([
{
id: 0,
appId: "daily-video",
type: "daily_video",
userId: null,
key: await getDailyAppKeys(),
},
]);
return videoAdapter?.createMeeting(calEvent);
};
export { getBusyVideoTimes, createMeeting, updateMeeting, deleteMeeting };