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:
co-authored by
Peer Richelsen
zomars
parent
74a16e28f4
commit
1c165bd934
@@ -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}`, {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user