Compare commits
1 Commits
main
...
feat/confe
Author | SHA1 | Date | |
---|---|---|---|
|
ef13cdfe85 |
|
@ -3,13 +3,23 @@ import async from "async";
|
|||
import merge from "lodash/merge";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
<<<<<<< HEAD:apps/web/lib/events/EventManager.ts
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import { FAKE_HUDDLE_CREDENTIAL } from "@lib/integrations/Huddle01/Huddle01VideoApiAdapter";
|
||||
=======
|
||||
>>>>>>> [CAL-770] add video conferencing integration improvement:lib/events/EventManager.ts
|
||||
import { createEvent, updateEvent } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { AdditionInformation, CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { createMeeting, updateMeeting } from "@lib/integrations/videoConferencing/VideoConferencingManager";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/videoConferencing/constants/generals";
|
||||
import { VideoCallData } from "@lib/integrations/videoConferencing/interfaces/VideoConferencing";
|
||||
import { LocationType } from "@lib/location";
|
||||
import prisma from "@lib/prisma";
|
||||
<<<<<<< HEAD:apps/web/lib/events/EventManager.ts
|
||||
import { createMeeting, updateMeeting, VideoCallData } from "@lib/videoClient";
|
||||
=======
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
>>>>>>> [CAL-770] add video conferencing integration improvement:lib/events/EventManager.ts
|
||||
|
||||
export type Event = AdditionInformation & VideoCallData;
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ import { TFunction } from "next-i18next";
|
|||
import { PaymentInfo } from "@ee/lib/stripe/server";
|
||||
|
||||
import type { Event } from "@lib/events/EventManager";
|
||||
import { VideoCallData } from "@lib/integrations/videoConferencing/interfaces/VideoConferencing";
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
import { VideoCallData } from "@lib/videoClient";
|
||||
|
||||
import { NewCalendarEventType } from "../constants/types";
|
||||
import { ConferenceData } from "./GoogleCalendar";
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// import { getBusyVideoTimes } from "@lib/videoClient";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
|
@ -67,8 +66,6 @@ export async function getUserAvailability(query: {
|
|||
selectedCalendars
|
||||
);
|
||||
|
||||
// busyTimes.push(...await getBusyVideoTimes(currentUser.credentials, dateFrom.format(), dateTo.format()));
|
||||
|
||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
||||
end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// import { getBusyVideoTimes } from "@lib/videoClient";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
|
@ -69,8 +68,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
selectedCalendars
|
||||
);
|
||||
|
||||
// busyTimes.push(...await getBusyVideoTimes(currentUser.credentials, dateFrom.format(), dateTo.format()));
|
||||
|
||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
||||
end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
|
||||
|
|
|
@ -23,11 +23,11 @@ import EventManager, { EventResult, PartialReference } from "@lib/events/EventMa
|
|||
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
|
||||
import { getBusyVideoTimes } from "@lib/integrations/videoConferencing/VideoConferencingManager";
|
||||
import logger from "@lib/logger";
|
||||
import notEmpty from "@lib/notEmpty";
|
||||
import prisma from "@lib/prisma";
|
||||
import { BookingCreateBody } from "@lib/types/booking";
|
||||
import { getBusyVideoTimes } from "@lib/videoClient";
|
||||
import sendPayload from "@lib/webhooks/sendPayload";
|
||||
import getSubscribers from "@lib/webhooks/subscriptions";
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@ import { refund } from "@ee/lib/stripe/server";
|
|||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { sendCancelledEmails } from "@lib/emails/email-manager";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { deleteMeeting } from "@lib/integrations/videoConferencing/VideoConferencingManager";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/videoConferencing/constants/generals";
|
||||
import prisma from "@lib/prisma";
|
||||
import { deleteMeeting } from "@lib/videoClient";
|
||||
import sendPayload from "@lib/webhooks/sendPayload";
|
||||
import getSubscribers from "@lib/webhooks/subscriptions";
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { BASE_URL } from "@lib/config/constants";
|
||||
import { VIDEO_CONFERENCING_INTEGRATIONS_TYPES } from "@lib/integrations/videoConferencing/constants/generals";
|
||||
|
||||
import prisma from "../../../../lib/prisma";
|
||||
|
||||
|
@ -43,7 +44,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
data: {
|
||||
credentials: {
|
||||
create: {
|
||||
type: "zoom_video",
|
||||
type: VIDEO_CONFERENCING_INTEGRATIONS_TYPES.zoom,
|
||||
key: responseBody,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -33,6 +33,7 @@ import { getSession } from "@lib/auth";
|
|||
import { HttpError } from "@lib/core/http/error";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations";
|
||||
import { VIDEO_CONFERENCING_INTEGRATIONS_TYPES } from "@lib/integrations/videoConferencing/constants/generals";
|
||||
import { LocationType } from "@lib/location";
|
||||
import showToast from "@lib/notification";
|
||||
import prisma from "@lib/prisma";
|
||||
|
@ -1668,12 +1669,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
|
||||
const locationOptions: OptionTypeBase[] = [];
|
||||
|
||||
<<<<<<< HEAD:apps/web/pages/event-types/[type].tsx
|
||||
if (hasIntegration(integrations, "zoom_video")) {
|
||||
locationOptions.push({
|
||||
value: LocationType.Zoom,
|
||||
label: "Zoom Video",
|
||||
disabled: true,
|
||||
});
|
||||
=======
|
||||
if (hasIntegration(integrations, VIDEO_CONFERENCING_INTEGRATIONS_TYPES.zoom)) {
|
||||
locationOptions.push({ value: LocationType.Zoom, label: "Zoom Video", disabled: true });
|
||||
>>>>>>> [CAL-770] add video conferencing integration improvement:pages/event-types/[type].tsx
|
||||
}
|
||||
const hasPaymentIntegration = hasIntegration(integrations, "stripe_payment");
|
||||
if (hasIntegration(integrations, "google_calendar")) {
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { getUid } from "@lib/CalEventParser";
|
||||
import { EventResult, PartialReference } from "@lib/events/EventManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { VIDEO_CONFERENCING_INTEGRATIONS_TYPES } from "@lib/integrations/videoConferencing/constants/generals";
|
||||
import { VideoConferencingServiceType } from "@lib/integrations/videoConferencing/constants/types";
|
||||
import VideoConferencingService from "@lib/integrations/videoConferencing/services/BaseVideoConferencingService";
|
||||
import DailyVideoConferencingService from "@lib/integrations/videoConferencing/services/DailyVideoConferencingService";
|
||||
import ZoomVideoConferencingService from "@lib/integrations/videoConferencing/services/ZoomVideoConferencingService";
|
||||
import logger from "@lib/logger";
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
||||
|
||||
const translator = short();
|
||||
|
||||
const VIDEO_CONFERENCING: Record<string, VideoConferencingServiceType> = {
|
||||
[VIDEO_CONFERENCING_INTEGRATIONS_TYPES.zoom]: ZoomVideoConferencingService,
|
||||
[VIDEO_CONFERENCING_INTEGRATIONS_TYPES.daily]: DailyVideoConferencingService,
|
||||
};
|
||||
|
||||
export const getVideoConferencing = (credential: Credential): VideoConferencingService | null => {
|
||||
const { type } = credential;
|
||||
|
||||
const videoConferencing = VIDEO_CONFERENCING[type];
|
||||
if (!videoConferencing) {
|
||||
log.warn(`video conferencing of type ${type} does not implemented`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new videoConferencing(credential);
|
||||
};
|
||||
|
||||
export const getBusyVideoTimes = (withCredentials: Credential[]) => {
|
||||
const appCredentials = withCredentials
|
||||
.map((credential) => getVideoConferencing(credential))
|
||||
.filter((valid) => valid) as VideoConferencingService[];
|
||||
|
||||
return Promise.all(appCredentials.map((c) => c.getAvailability())).then((results) =>
|
||||
results.reduce((acc, availability) => acc.concat(availability), [])
|
||||
);
|
||||
};
|
||||
|
||||
export const createMeeting = async (
|
||||
credential: Credential,
|
||||
calEvent: Ensure<CalendarEvent, "language">
|
||||
): Promise<EventResult> => {
|
||||
const uid: string = getUid(calEvent);
|
||||
|
||||
if (!credential) {
|
||||
throw new Error(
|
||||
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
|
||||
);
|
||||
}
|
||||
|
||||
const adapter = getVideoConferencing(credential);
|
||||
const createdMeeting = await adapter?.createMeeting(calEvent).catch((e: Error) => {
|
||||
log.error("createMeeting failed", e, calEvent);
|
||||
});
|
||||
|
||||
if (!createdMeeting) {
|
||||
return {
|
||||
type: credential.type,
|
||||
success: false,
|
||||
uid,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success: true,
|
||||
uid,
|
||||
createdEvent: createdMeeting,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
export const updateMeeting = async (
|
||||
credential: Credential,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRef: PartialReference | null
|
||||
): Promise<EventResult> => {
|
||||
const uid = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
|
||||
|
||||
let success = true;
|
||||
|
||||
const adapter = getVideoConferencing(credential);
|
||||
const updatedMeeting =
|
||||
credential && bookingRef
|
||||
? await adapter?.updateMeeting(bookingRef, calEvent).catch((e: Error) => {
|
||||
log.error("updateMeeting failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (!updatedMeeting) {
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
updatedEvent: updatedMeeting,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteMeeting = (credential: Credential, uid: string): Promise<unknown> => {
|
||||
if (credential) {
|
||||
const adapter = getVideoConferencing(credential);
|
||||
|
||||
if (adapter) {
|
||||
return adapter.deleteMeeting(uid);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export const DEFAULT_VIDEO_CONFERENCING_INTEGRATION_NAME = "";
|
|
@ -0,0 +1,13 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
export const VIDEO_CONFERENCING_INTEGRATIONS_TYPES = {
|
||||
zoom: "zoom_video",
|
||||
daily: "daily_video",
|
||||
};
|
||||
|
||||
export const FAKE_DAILY_CREDENTIAL: Credential = {
|
||||
id: +new Date().getTime(),
|
||||
type: "daily_video",
|
||||
key: { apikey: process.env.DAILY_API_KEY },
|
||||
userId: +new Date().getTime(),
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import DailyVideoConferencingService from "@lib/integrations/videoConferencing/services/DailyVideoConferencingService";
|
||||
import ZoomVideoConferencingService from "@lib/integrations/videoConferencing/services/ZoomVideoConferencingService";
|
||||
|
||||
export type VideoConferencingServiceType =
|
||||
| typeof ZoomVideoConferencingService
|
||||
| typeof DailyVideoConferencingService;
|
|
@ -0,0 +1,71 @@
|
|||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
|
||||
type EventBusyDate = Record<"start" | "end", Date>;
|
||||
|
||||
export interface VideoCallData {
|
||||
type: string;
|
||||
id: string;
|
||||
password: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ZoomToken {
|
||||
scope: "meeting:write";
|
||||
expiry_date: number;
|
||||
expires_in?: number; // deprecated, purely for backwards compatibility; superseeded by expiry_date.
|
||||
token_type: "bearer";
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface DailyReturnType {
|
||||
/** Long UID string ie: 987b5eb5-d116-4a4e-8e2c-14fcb5710966 */
|
||||
id: string;
|
||||
/** Not a real name, just a random generated string ie: "ePR84NQ1bPigp79dDezz" */
|
||||
name: string;
|
||||
api_created: boolean;
|
||||
privacy: "private" | "public";
|
||||
/** https://api-demo.daily.co/ePR84NQ1bPigp79dDezz */
|
||||
url: string;
|
||||
created_at: string;
|
||||
config: {
|
||||
nbf: number;
|
||||
exp: number;
|
||||
enable_chat: boolean;
|
||||
enable_knocking: boolean;
|
||||
enable_prejoin_ui: boolean;
|
||||
enable_new_call_ui: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DailyEventResult {
|
||||
id: string;
|
||||
name: string;
|
||||
api_created: boolean;
|
||||
privacy: string;
|
||||
url: string;
|
||||
created_at: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DailyVideoCallData {
|
||||
type: string;
|
||||
id: string;
|
||||
password: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type DailyKey = {
|
||||
apikey: string;
|
||||
};
|
||||
|
||||
export interface VideoConferencing {
|
||||
createMeeting(event: CalendarEvent): Promise<VideoCallData>;
|
||||
|
||||
updateMeeting(bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData>;
|
||||
|
||||
deleteMeeting(uid: string): Promise<unknown>;
|
||||
|
||||
getAvailability(dateFrom?: string, dateTo?: string): Promise<EventBusyDate[]>;
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { DEFAULT_VIDEO_CONFERENCING_INTEGRATION_NAME } from "@lib/integrations/videoConferencing/constants/defaults";
|
||||
import {
|
||||
VideoCallData,
|
||||
VideoConferencing,
|
||||
} from "@lib/integrations/videoConferencing/interfaces/VideoConferencing";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
export default abstract class BaseVideoConferencingService implements VideoConferencing {
|
||||
protected integrationName = DEFAULT_VIDEO_CONFERENCING_INTEGRATION_NAME;
|
||||
|
||||
log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
|
||||
constructor(credential: Credential, integrationName: string) {
|
||||
this.integrationName = integrationName;
|
||||
}
|
||||
createMeeting(event: CalendarEvent): Promise<VideoCallData> {
|
||||
this.log.info(`creating meeting with ${JSON.stringify(event)}`);
|
||||
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
updateMeeting(bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> {
|
||||
this.log.info(
|
||||
`updating meeting with bookingRef ${JSON.stringify(bookingRef)} and event ${JSON.stringify(event)}`
|
||||
);
|
||||
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
deleteMeeting(uid: string): Promise<unknown> {
|
||||
this.log.info(`deleting meeting with uid ${uid}`);
|
||||
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getAvailability(dateFrom?: string, dateTo?: string): Promise<{ start: Date; end: Date }[]> {
|
||||
this.log.info(`get meeting availability between ${dateFrom} and ${dateTo}`);
|
||||
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { BASE_URL } from "@lib/config/constants";
|
||||
import { handleErrorsJson } from "@lib/errors";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { VIDEO_CONFERENCING_INTEGRATIONS_TYPES } from "@lib/integrations/videoConferencing/constants/generals";
|
||||
import {
|
||||
DailyKey,
|
||||
DailyReturnType,
|
||||
VideoCallData,
|
||||
} from "@lib/integrations/videoConferencing/interfaces/VideoConferencing";
|
||||
import BaseVideoConferencingService from "@lib/integrations/videoConferencing/services/BaseVideoConferencingService";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export default class DailyVideoConferencingService extends BaseVideoConferencingService {
|
||||
private dailyApiToken = "";
|
||||
|
||||
constructor(credential: Credential) {
|
||||
super(credential, VIDEO_CONFERENCING_INTEGRATIONS_TYPES.daily);
|
||||
|
||||
this.dailyApiToken = (credential.key as DailyKey).apikey;
|
||||
}
|
||||
|
||||
private async postToDailyAPI(endpoint: string, body: Record<string, any>) {
|
||||
return fetch("https://api.daily.co/v1" + endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + this.dailyApiToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
private translateEvent(event: CalendarEvent) {
|
||||
// Documentation at: https://docs.daily.co/reference#list-rooms
|
||||
// added a 1 hour buffer for room expiration and room entry
|
||||
const exp = Math.round(new Date(event.endTime).getTime() / 1000) + 60 * 60;
|
||||
const nbf = Math.round(new Date(event.startTime).getTime() / 1000) - 60 * 60;
|
||||
const scalePlan = process.env.DAILY_SCALE_PLAN;
|
||||
|
||||
if (scalePlan === "true") {
|
||||
return {
|
||||
privacy: "private",
|
||||
properties: {
|
||||
enable_new_call_ui: true,
|
||||
enable_prejoin_ui: true,
|
||||
enable_knocking: true,
|
||||
enable_screenshare: true,
|
||||
enable_chat: true,
|
||||
exp: exp,
|
||||
nbf: nbf,
|
||||
enable_recording: "local",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
privacy: "private",
|
||||
properties: {
|
||||
enable_new_call_ui: true,
|
||||
enable_prejoin_ui: true,
|
||||
enable_knocking: true,
|
||||
enable_screenshare: true,
|
||||
enable_chat: true,
|
||||
exp: exp,
|
||||
nbf: nbf,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async createOrUpdateMeeting(endpoint: string, event: CalendarEvent): Promise<VideoCallData> {
|
||||
if (!event.uid) {
|
||||
throw new Error("We need need the booking uid to create the Daily reference in DB");
|
||||
}
|
||||
const response = await this.postToDailyAPI(endpoint, this.translateEvent(event));
|
||||
const dailyEvent: DailyReturnType = await handleErrorsJson(response);
|
||||
const res = await this.postToDailyAPI("/meeting-tokens", {
|
||||
properties: { room_name: dailyEvent.name, is_owner: true },
|
||||
});
|
||||
const meetingToken: { token: string } = await handleErrorsJson(res);
|
||||
await prisma.dailyEventReference.create({
|
||||
data: {
|
||||
dailyurl: dailyEvent.url,
|
||||
dailytoken: meetingToken.token,
|
||||
booking: {
|
||||
connect: {
|
||||
uid: event.uid,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.resolve({
|
||||
type: "daily_video",
|
||||
id: dailyEvent.name,
|
||||
password: "",
|
||||
url: BASE_URL + "/call/" + event.uid,
|
||||
});
|
||||
}
|
||||
|
||||
async createMeeting(event: CalendarEvent): Promise<VideoCallData> {
|
||||
return this.createOrUpdateMeeting("/rooms", event);
|
||||
}
|
||||
|
||||
async updateMeeting(bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> {
|
||||
return this.createOrUpdateMeeting("/rooms/" + bookingRef.uid, event);
|
||||
}
|
||||
|
||||
async deleteMeeting(uid: string): Promise<void> {
|
||||
await fetch("https://api.daily.co/v1/rooms/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + this.dailyApiToken,
|
||||
},
|
||||
}).then(handleErrorsJson);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getAvailability() {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { VIDEO_CONFERENCING_INTEGRATIONS_TYPES } from "@lib/integrations/videoConferencing/constants/generals";
|
||||
import { VideoCallData, ZoomToken } from "@lib/integrations/videoConferencing/interfaces/VideoConferencing";
|
||||
import BaseVideoConferencingService from "@lib/integrations/videoConferencing/services/BaseVideoConferencingService";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export default class ZoomVideoConferencingService extends BaseVideoConferencingService {
|
||||
private auth;
|
||||
|
||||
constructor(credential: Credential) {
|
||||
super(credential, VIDEO_CONFERENCING_INTEGRATIONS_TYPES.zoom);
|
||||
|
||||
this.auth = this.zoomAuth(credential);
|
||||
}
|
||||
|
||||
private zoomAuth = (credential: Credential) => {
|
||||
const credentialKey = credential.key as unknown as ZoomToken;
|
||||
const isTokenValid = (token: ZoomToken) =>
|
||||
token && token.token_type && token.access_token && (token.expires_in || token.expiry_date) < Date.now();
|
||||
const authHeader =
|
||||
"Basic " +
|
||||
Buffer.from(process.env.ZOOM_CLIENT_ID + ":" + process.env.ZOOM_CLIENT_SECRET).toString("base64");
|
||||
|
||||
const refreshAccessToken = (refreshToken: string) =>
|
||||
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;
|
||||
});
|
||||
|
||||
return {
|
||||
getToken: () =>
|
||||
!isTokenValid(credentialKey)
|
||||
? Promise.resolve(credentialKey.access_token)
|
||||
: refreshAccessToken(credentialKey.refresh_token),
|
||||
};
|
||||
};
|
||||
|
||||
private 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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
async createMeeting(event: CalendarEvent): Promise<VideoCallData> {
|
||||
const accessToken = await this.auth.getToken();
|
||||
|
||||
const result = await fetch("https://api.zoom.us/v2/users/me/meetings", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(this.translateEvent(event)),
|
||||
}).then(handleErrorsJson);
|
||||
|
||||
return Promise.resolve({
|
||||
type: VIDEO_CONFERENCING_INTEGRATIONS_TYPES.zoom,
|
||||
id: result.id as string,
|
||||
password: result.password ?? "",
|
||||
url: result.join_url,
|
||||
});
|
||||
}
|
||||
|
||||
async updateMeeting(bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> {
|
||||
const accessToken = await this.auth.getToken();
|
||||
|
||||
await fetch("https://api.zoom.us/v2/meetings/" + bookingRef.uid, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(this.translateEvent(event)),
|
||||
}).then(handleErrorsRaw);
|
||||
|
||||
return Promise.resolve({
|
||||
type: VIDEO_CONFERENCING_INTEGRATIONS_TYPES.zoom,
|
||||
id: bookingRef.meetingId as string,
|
||||
password: bookingRef.meetingPassword as string,
|
||||
url: bookingRef.meetingUrl as string,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteMeeting(uid: string): Promise<void> {
|
||||
const accessToken = await this.auth.getToken();
|
||||
|
||||
await fetch("https://api.zoom.us/v2/meetings/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
}).then(handleErrorsRaw);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getAvailability() {
|
||||
return this.auth
|
||||
.getToken()
|
||||
.then(
|
||||
// TODO Possibly implement pagination for cases when there are more than 300 meetings already scheduled.
|
||||
(accessToken) =>
|
||||
fetch("https://api.zoom.us/v2/users/me/meetings?type=scheduled&page_size=300", {
|
||||
method: "get",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody) => {
|
||||
return responseBody.meetings.map((meeting: { start_time: string; duration: number }) => ({
|
||||
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 [];
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user