[CAL-770] add video conferencing integration improvement
This commit is contained in:
parent
97550a39f3
commit
ef13cdfe85
|
@ -3,13 +3,23 @@ import async from "async";
|
||||||
import merge from "lodash/merge";
|
import merge from "lodash/merge";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
|
<<<<<<< HEAD:apps/web/lib/events/EventManager.ts
|
||||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||||
import { FAKE_HUDDLE_CREDENTIAL } from "@lib/integrations/Huddle01/Huddle01VideoApiAdapter";
|
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 { createEvent, updateEvent } from "@lib/integrations/calendar/CalendarManager";
|
||||||
import { AdditionInformation, CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
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 { LocationType } from "@lib/location";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
<<<<<<< HEAD:apps/web/lib/events/EventManager.ts
|
||||||
import { createMeeting, updateMeeting, VideoCallData } from "@lib/videoClient";
|
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;
|
export type Event = AdditionInformation & VideoCallData;
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { TFunction } from "next-i18next";
|
||||||
import { PaymentInfo } from "@ee/lib/stripe/server";
|
import { PaymentInfo } from "@ee/lib/stripe/server";
|
||||||
|
|
||||||
import type { Event } from "@lib/events/EventManager";
|
import type { Event } from "@lib/events/EventManager";
|
||||||
|
import { VideoCallData } from "@lib/integrations/videoConferencing/interfaces/VideoConferencing";
|
||||||
import { Ensure } from "@lib/types/utils";
|
import { Ensure } from "@lib/types/utils";
|
||||||
import { VideoCallData } from "@lib/videoClient";
|
|
||||||
|
|
||||||
import { NewCalendarEventType } from "../constants/types";
|
import { NewCalendarEventType } from "../constants/types";
|
||||||
import { ConferenceData } from "./GoogleCalendar";
|
import { ConferenceData } from "./GoogleCalendar";
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
// import { getBusyVideoTimes } from "@lib/videoClient";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
@ -67,8 +66,6 @@ export async function getUserAvailability(query: {
|
||||||
selectedCalendars
|
selectedCalendars
|
||||||
);
|
);
|
||||||
|
|
||||||
// busyTimes.push(...await getBusyVideoTimes(currentUser.credentials, dateFrom.format(), dateTo.format()));
|
|
||||||
|
|
||||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
||||||
end: dayjs(a.end).add(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 { Prisma } from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
@ -69,8 +68,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
selectedCalendars
|
selectedCalendars
|
||||||
);
|
);
|
||||||
|
|
||||||
// busyTimes.push(...await getBusyVideoTimes(currentUser.credentials, dateFrom.format(), dateTo.format()));
|
|
||||||
|
|
||||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
||||||
end: dayjs(a.end).add(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 { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||||
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||||
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
|
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
|
||||||
|
import { getBusyVideoTimes } from "@lib/integrations/videoConferencing/VideoConferencingManager";
|
||||||
import logger from "@lib/logger";
|
import logger from "@lib/logger";
|
||||||
import notEmpty from "@lib/notEmpty";
|
import notEmpty from "@lib/notEmpty";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { BookingCreateBody } from "@lib/types/booking";
|
import { BookingCreateBody } from "@lib/types/booking";
|
||||||
import { getBusyVideoTimes } from "@lib/videoClient";
|
|
||||||
import sendPayload from "@lib/webhooks/sendPayload";
|
import sendPayload from "@lib/webhooks/sendPayload";
|
||||||
import getSubscribers from "@lib/webhooks/subscriptions";
|
import getSubscribers from "@lib/webhooks/subscriptions";
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ import { refund } from "@ee/lib/stripe/server";
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { sendCancelledEmails } from "@lib/emails/email-manager";
|
import { sendCancelledEmails } from "@lib/emails/email-manager";
|
||||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
|
||||||
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
||||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
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 prisma from "@lib/prisma";
|
||||||
import { deleteMeeting } from "@lib/videoClient";
|
|
||||||
import sendPayload from "@lib/webhooks/sendPayload";
|
import sendPayload from "@lib/webhooks/sendPayload";
|
||||||
import getSubscribers from "@lib/webhooks/subscriptions";
|
import getSubscribers from "@lib/webhooks/subscriptions";
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { BASE_URL } from "@lib/config/constants";
|
import { BASE_URL } from "@lib/config/constants";
|
||||||
|
import { VIDEO_CONFERENCING_INTEGRATIONS_TYPES } from "@lib/integrations/videoConferencing/constants/generals";
|
||||||
|
|
||||||
import prisma from "../../../../lib/prisma";
|
import prisma from "../../../../lib/prisma";
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
data: {
|
data: {
|
||||||
credentials: {
|
credentials: {
|
||||||
create: {
|
create: {
|
||||||
type: "zoom_video",
|
type: VIDEO_CONFERENCING_INTEGRATIONS_TYPES.zoom,
|
||||||
key: responseBody,
|
key: responseBody,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { getSession } from "@lib/auth";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations";
|
import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations";
|
||||||
|
import { VIDEO_CONFERENCING_INTEGRATIONS_TYPES } from "@lib/integrations/videoConferencing/constants/generals";
|
||||||
import { LocationType } from "@lib/location";
|
import { LocationType } from "@lib/location";
|
||||||
import showToast from "@lib/notification";
|
import showToast from "@lib/notification";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
@ -1668,12 +1669,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
|
|
||||||
const locationOptions: OptionTypeBase[] = [];
|
const locationOptions: OptionTypeBase[] = [];
|
||||||
|
|
||||||
|
<<<<<<< HEAD:apps/web/pages/event-types/[type].tsx
|
||||||
if (hasIntegration(integrations, "zoom_video")) {
|
if (hasIntegration(integrations, "zoom_video")) {
|
||||||
locationOptions.push({
|
locationOptions.push({
|
||||||
value: LocationType.Zoom,
|
value: LocationType.Zoom,
|
||||||
label: "Zoom Video",
|
label: "Zoom Video",
|
||||||
disabled: true,
|
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");
|
const hasPaymentIntegration = hasIntegration(integrations, "stripe_payment");
|
||||||
if (hasIntegration(integrations, "google_calendar")) {
|
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