Refactors video integrations (#1037)
* Fixes error types * Type fixes * Refactors video meeting handling * More type fixes * Type fixes * More fixes * Makes language non optional * Adds missing translations * Apply suggestions from code review Co-authored-by: Alex Johansson <alexander@n1s.se> * Feedback Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Alex Johansson <alexander@n1s.se>
This commit is contained in:
		
							parent
							
								
									eabb096e14
								
							
						
					
					
						commit
						e38086b8fe
					
				
					 17 changed files with 618 additions and 612 deletions
				
			
		| 
						 | 
					@ -9,6 +9,9 @@ import { HttpError } from "@lib/core/http/error";
 | 
				
			||||||
import { getErrorFromUnknown } from "@lib/errors";
 | 
					import { getErrorFromUnknown } from "@lib/errors";
 | 
				
			||||||
import EventManager from "@lib/events/EventManager";
 | 
					import EventManager from "@lib/events/EventManager";
 | 
				
			||||||
import prisma from "@lib/prisma";
 | 
					import prisma from "@lib/prisma";
 | 
				
			||||||
 | 
					import { Ensure } from "@lib/types/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getTranslation } from "@server/lib/i18n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const config = {
 | 
					export const config = {
 | 
				
			||||||
  api: {
 | 
					  api: {
 | 
				
			||||||
| 
						 | 
					@ -69,7 +72,9 @@ async function handlePaymentSuccess(event: Stripe.Event) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!user) throw new Error("No user found");
 | 
					  if (!user) throw new Error("No user found");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const evt: CalendarEvent = {
 | 
					  const t = await getTranslation(/* FIXME handle mulitple locales here */ "en", "common");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const evt: Ensure<CalendarEvent, "language"> = {
 | 
				
			||||||
    type: booking.title,
 | 
					    type: booking.title,
 | 
				
			||||||
    title: booking.title,
 | 
					    title: booking.title,
 | 
				
			||||||
    description: booking.description || undefined,
 | 
					    description: booking.description || undefined,
 | 
				
			||||||
| 
						 | 
					@ -77,12 +82,14 @@ async function handlePaymentSuccess(event: Stripe.Event) {
 | 
				
			||||||
    endTime: booking.endTime.toISOString(),
 | 
					    endTime: booking.endTime.toISOString(),
 | 
				
			||||||
    organizer: { email: user.email!, name: user.name!, timeZone: user.timeZone },
 | 
					    organizer: { email: user.email!, name: user.name!, timeZone: user.timeZone },
 | 
				
			||||||
    attendees: booking.attendees,
 | 
					    attendees: booking.attendees,
 | 
				
			||||||
 | 
					    uid: booking.uid,
 | 
				
			||||||
 | 
					    language: t,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  if (booking.location) evt.location = booking.location;
 | 
					  if (booking.location) evt.location = booking.location;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (booking.confirmed) {
 | 
					  if (booking.confirmed) {
 | 
				
			||||||
    const eventManager = new EventManager(user.credentials);
 | 
					    const eventManager = new EventManager(user.credentials);
 | 
				
			||||||
    const scheduleResult = await eventManager.create(evt, booking.uid);
 | 
					    const scheduleResult = await eventManager.create(evt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await prisma.booking.update({
 | 
					    await prisma.booking.update({
 | 
				
			||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,11 @@
 | 
				
			||||||
import { Credential } from "@prisma/client";
 | 
					/* eslint-disable @typescript-eslint/ban-ts-comment */
 | 
				
			||||||
 | 
					import { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
 | 
				
			||||||
 | 
					import { Credential, Prisma, SelectedCalendar } from "@prisma/client";
 | 
				
			||||||
 | 
					import { GetTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
 | 
				
			||||||
 | 
					import { Auth, calendar_v3, google } from "googleapis";
 | 
				
			||||||
import { TFunction } from "next-i18next";
 | 
					import { TFunction } from "next-i18next";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { EventResult } from "@lib/events/EventManager";
 | 
					import { Event, EventResult } from "@lib/events/EventManager";
 | 
				
			||||||
import logger from "@lib/logger";
 | 
					import logger from "@lib/logger";
 | 
				
			||||||
import { VideoCallData } from "@lib/videoClient";
 | 
					import { VideoCallData } from "@lib/videoClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,34 +18,34 @@ import prisma from "./prisma";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] });
 | 
					const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
 | 
					const googleAuth = (credential: Credential) => {
 | 
				
			||||||
const { google } = require("googleapis");
 | 
					  const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS!).web;
 | 
				
			||||||
 | 
					 | 
				
			||||||
const googleAuth = (credential) => {
 | 
					 | 
				
			||||||
  const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web;
 | 
					 | 
				
			||||||
  const myGoogleAuth = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
 | 
					  const myGoogleAuth = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
 | 
				
			||||||
  myGoogleAuth.setCredentials(credential.key);
 | 
					  const googleCredentials = credential.key as Auth.Credentials;
 | 
				
			||||||
 | 
					  myGoogleAuth.setCredentials(googleCredentials);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // FIXME - type errors IDK Why this is a protected method ¯\_(ツ)_/¯
 | 
				
			||||||
  const isExpired = () => myGoogleAuth.isTokenExpiring();
 | 
					  const isExpired = () => myGoogleAuth.isTokenExpiring();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const refreshAccessToken = () =>
 | 
					  const refreshAccessToken = () =>
 | 
				
			||||||
    myGoogleAuth
 | 
					    myGoogleAuth
 | 
				
			||||||
      .refreshToken(credential.key.refresh_token)
 | 
					      // FIXME - type errors IDK Why this is a protected method ¯\_(ツ)_/¯
 | 
				
			||||||
      .then((res) => {
 | 
					      .refreshToken(googleCredentials.refresh_token)
 | 
				
			||||||
        const token = res.res.data;
 | 
					      .then((res: GetTokenResponse) => {
 | 
				
			||||||
        credential.key.access_token = token.access_token;
 | 
					        const token = res.res?.data;
 | 
				
			||||||
        credential.key.expiry_date = token.expiry_date;
 | 
					        googleCredentials.access_token = token.access_token;
 | 
				
			||||||
 | 
					        googleCredentials.expiry_date = token.expiry_date;
 | 
				
			||||||
        return prisma.credential
 | 
					        return prisma.credential
 | 
				
			||||||
          .update({
 | 
					          .update({
 | 
				
			||||||
            where: {
 | 
					            where: {
 | 
				
			||||||
              id: credential.id,
 | 
					              id: credential.id,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            data: {
 | 
					            data: {
 | 
				
			||||||
              key: credential.key,
 | 
					              key: googleCredentials as Prisma.InputJsonValue,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
          .then(() => {
 | 
					          .then(() => {
 | 
				
			||||||
            myGoogleAuth.setCredentials(credential.key);
 | 
					            myGoogleAuth.setCredentials(googleCredentials);
 | 
				
			||||||
            return myGoogleAuth;
 | 
					            return myGoogleAuth;
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
| 
						 | 
					@ -71,13 +75,21 @@ function handleErrorsRaw(response: Response) {
 | 
				
			||||||
  return response.text();
 | 
					  return response.text();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const o365Auth = (credential) => {
 | 
					type O365AuthCredentials = {
 | 
				
			||||||
  const isExpired = (expiryDate) => expiryDate < Math.round(+new Date() / 1000);
 | 
					  expiry_date: number;
 | 
				
			||||||
 | 
					  access_token: string;
 | 
				
			||||||
 | 
					  refresh_token: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const refreshAccessToken = (refreshToken) => {
 | 
					const o365Auth = (credential: Credential) => {
 | 
				
			||||||
 | 
					  const isExpired = (expiryDate: number) => expiryDate < Math.round(+new Date() / 1000);
 | 
				
			||||||
 | 
					  const o365AuthCredentials = credential.key as O365AuthCredentials;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const refreshAccessToken = (refreshToken: string) => {
 | 
				
			||||||
    return fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
 | 
					    return fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
 | 
					      headers: { "Content-Type": "application/x-www-form-urlencoded" },
 | 
				
			||||||
 | 
					      // FIXME types - IDK how to type this TBH
 | 
				
			||||||
      body: new URLSearchParams({
 | 
					      body: new URLSearchParams({
 | 
				
			||||||
        scope: "User.Read Calendars.Read Calendars.ReadWrite",
 | 
					        scope: "User.Read Calendars.Read Calendars.ReadWrite",
 | 
				
			||||||
        client_id: process.env.MS_GRAPH_CLIENT_ID,
 | 
					        client_id: process.env.MS_GRAPH_CLIENT_ID,
 | 
				
			||||||
| 
						 | 
					@ -88,26 +100,26 @@ const o365Auth = (credential) => {
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
      .then(handleErrorsJson)
 | 
					      .then(handleErrorsJson)
 | 
				
			||||||
      .then((responseBody) => {
 | 
					      .then((responseBody) => {
 | 
				
			||||||
        credential.key.access_token = responseBody.access_token;
 | 
					        o365AuthCredentials.access_token = responseBody.access_token;
 | 
				
			||||||
        credential.key.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in);
 | 
					        o365AuthCredentials.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in);
 | 
				
			||||||
        return prisma.credential
 | 
					        return prisma.credential
 | 
				
			||||||
          .update({
 | 
					          .update({
 | 
				
			||||||
            where: {
 | 
					            where: {
 | 
				
			||||||
              id: credential.id,
 | 
					              id: credential.id,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            data: {
 | 
					            data: {
 | 
				
			||||||
              key: credential.key,
 | 
					              key: o365AuthCredentials,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
          .then(() => credential.key.access_token);
 | 
					          .then(() => o365AuthCredentials.access_token);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    getToken: () =>
 | 
					    getToken: () =>
 | 
				
			||||||
      !isExpired(credential.key.expiry_date)
 | 
					      !isExpired(o365AuthCredentials.expiry_date)
 | 
				
			||||||
        ? Promise.resolve(credential.key.access_token)
 | 
					        ? Promise.resolve(o365AuthCredentials.access_token)
 | 
				
			||||||
        : refreshAccessToken(credential.key.refresh_token),
 | 
					        : refreshAccessToken(o365AuthCredentials.refresh_token),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -146,24 +158,22 @@ export interface CalendarEvent {
 | 
				
			||||||
  conferenceData?: ConferenceData;
 | 
					  conferenceData?: ConferenceData;
 | 
				
			||||||
  language: TFunction;
 | 
					  language: TFunction;
 | 
				
			||||||
  additionInformation?: AdditionInformation;
 | 
					  additionInformation?: AdditionInformation;
 | 
				
			||||||
 | 
					  /** If this property exist it we can assume it's a reschedule/update */
 | 
				
			||||||
  uid?: string | null;
 | 
					  uid?: string | null;
 | 
				
			||||||
  videoCallData?: VideoCallData;
 | 
					  videoCallData?: VideoCallData;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ConferenceData {
 | 
					export interface ConferenceData {
 | 
				
			||||||
  createRequest: unknown;
 | 
					  createRequest: calendar_v3.Schema$CreateConferenceRequest;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					export interface IntegrationCalendar extends Partial<SelectedCalendar> {
 | 
				
			||||||
export interface IntegrationCalendar {
 | 
					  primary?: boolean;
 | 
				
			||||||
  integration: string;
 | 
					  name?: string;
 | 
				
			||||||
  primary: boolean;
 | 
					 | 
				
			||||||
  externalId: string;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type BufferedBusyTime = { start: string; end: string };
 | 
					type BufferedBusyTime = { start: string; end: string };
 | 
				
			||||||
export interface CalendarApiAdapter {
 | 
					export interface CalendarApiAdapter {
 | 
				
			||||||
  createEvent(event: CalendarEvent): Promise<unknown>;
 | 
					  createEvent(event: CalendarEvent): Promise<Event>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateEvent(uid: string, event: CalendarEvent): Promise<any>;
 | 
					  updateEvent(uid: string, event: CalendarEvent): Promise<any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -178,15 +188,10 @@ export interface CalendarApiAdapter {
 | 
				
			||||||
  listCalendars(): Promise<IntegrationCalendar[]>;
 | 
					  listCalendars(): Promise<IntegrationCalendar[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => {
 | 
					const MicrosoftOffice365Calendar = (credential: Credential): CalendarApiAdapter => {
 | 
				
			||||||
  const auth = o365Auth(credential);
 | 
					  const auth = o365Auth(credential);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const translateEvent = (event: CalendarEvent) => {
 | 
					  const translateEvent = (event: CalendarEvent) => {
 | 
				
			||||||
    const optional = {};
 | 
					 | 
				
			||||||
    if (event.location) {
 | 
					 | 
				
			||||||
      optional.location = { displayName: event.location };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      subject: event.title,
 | 
					      subject: event.title,
 | 
				
			||||||
      body: {
 | 
					      body: {
 | 
				
			||||||
| 
						 | 
					@ -208,7 +213,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        type: "required",
 | 
					        type: "required",
 | 
				
			||||||
      })),
 | 
					      })),
 | 
				
			||||||
      ...optional,
 | 
					      location: event.location ? { displayName: event.location } : undefined,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -224,13 +229,13 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
        .then(handleErrorsJson)
 | 
					        .then(handleErrorsJson)
 | 
				
			||||||
        .then((responseBody) => {
 | 
					        .then((responseBody: { value: OfficeCalendar[] }) => {
 | 
				
			||||||
          return responseBody.value.map((cal) => {
 | 
					          return responseBody.value.map((cal) => {
 | 
				
			||||||
            const calendar: IntegrationCalendar = {
 | 
					            const calendar: IntegrationCalendar = {
 | 
				
			||||||
              externalId: cal.id,
 | 
					              externalId: cal.id ?? "No Id",
 | 
				
			||||||
              integration: integrationType,
 | 
					              integration: integrationType,
 | 
				
			||||||
              name: cal.name,
 | 
					              name: cal.name ?? "No calendar name",
 | 
				
			||||||
              primary: cal.isDefaultCalendar,
 | 
					              primary: cal.isDefaultCalendar ?? false,
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            return calendar;
 | 
					            return calendar;
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
| 
						 | 
					@ -248,17 +253,18 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
        .then((accessToken) => {
 | 
					        .then((accessToken) => {
 | 
				
			||||||
          const selectedCalendarIds = selectedCalendars
 | 
					          const selectedCalendarIds = selectedCalendars
 | 
				
			||||||
            .filter((e) => e.integration === integrationType)
 | 
					            .filter((e) => e.integration === integrationType)
 | 
				
			||||||
            .map((e) => e.externalId);
 | 
					            .map((e) => e.externalId)
 | 
				
			||||||
          if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0) {
 | 
					            .filter(Boolean);
 | 
				
			||||||
 | 
					          if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
 | 
				
			||||||
            // Only calendars of other integrations selected
 | 
					            // Only calendars of other integrations selected
 | 
				
			||||||
            return Promise.resolve([]);
 | 
					            return Promise.resolve([]);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            selectedCalendarIds.length == 0
 | 
					            selectedCalendarIds.length === 0
 | 
				
			||||||
              ? listCalendars().then((cals) => cals.map((e) => e.externalId))
 | 
					              ? listCalendars().then((cals) => cals.map((e) => e.externalId).filter(Boolean) || [])
 | 
				
			||||||
              : Promise.resolve(selectedCalendarIds).then((x) => x)
 | 
					              : Promise.resolve(selectedCalendarIds)
 | 
				
			||||||
          ).then((ids: string[]) => {
 | 
					          ).then((ids) => {
 | 
				
			||||||
            const requests = ids.map((calendarId, id) => ({
 | 
					            const requests = ids.map((calendarId, id) => ({
 | 
				
			||||||
              id,
 | 
					              id,
 | 
				
			||||||
              method: "GET",
 | 
					              method: "GET",
 | 
				
			||||||
| 
						 | 
					@ -268,6 +274,13 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
              url: `/me/calendars/${calendarId}/calendarView${filter}`,
 | 
					              url: `/me/calendars/${calendarId}/calendarView${filter}`,
 | 
				
			||||||
            }));
 | 
					            }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            type BatchResponse = {
 | 
				
			||||||
 | 
					              responses: SubResponse[];
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            type SubResponse = {
 | 
				
			||||||
 | 
					              body: { value: { start: { dateTime: string }; end: { dateTime: string } }[] };
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return fetch("https://graph.microsoft.com/v1.0/$batch", {
 | 
					            return fetch("https://graph.microsoft.com/v1.0/$batch", {
 | 
				
			||||||
              method: "POST",
 | 
					              method: "POST",
 | 
				
			||||||
              headers: {
 | 
					              headers: {
 | 
				
			||||||
| 
						 | 
					@ -277,9 +290,9 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
              body: JSON.stringify({ requests }),
 | 
					              body: JSON.stringify({ requests }),
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
              .then(handleErrorsJson)
 | 
					              .then(handleErrorsJson)
 | 
				
			||||||
              .then((responseBody) =>
 | 
					              .then((responseBody: BatchResponse) =>
 | 
				
			||||||
                responseBody.responses.reduce(
 | 
					                responseBody.responses.reduce(
 | 
				
			||||||
                  (acc, subResponse) =>
 | 
					                  (acc: BufferedBusyTime[], subResponse) =>
 | 
				
			||||||
                    acc.concat(
 | 
					                    acc.concat(
 | 
				
			||||||
                      subResponse.body.value.map((evt) => {
 | 
					                      subResponse.body.value.map((evt) => {
 | 
				
			||||||
                        return {
 | 
					                        return {
 | 
				
			||||||
| 
						 | 
					@ -295,6 +308,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .catch((err) => {
 | 
					        .catch((err) => {
 | 
				
			||||||
          console.log(err);
 | 
					          console.log(err);
 | 
				
			||||||
 | 
					          return Promise.reject([]);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    createEvent: (event: CalendarEvent) =>
 | 
					    createEvent: (event: CalendarEvent) =>
 | 
				
			||||||
| 
						 | 
					@ -337,7 +351,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const GoogleCalendar = (credential): CalendarApiAdapter => {
 | 
					const GoogleCalendar = (credential: Credential): CalendarApiAdapter => {
 | 
				
			||||||
  const auth = googleAuth(credential);
 | 
					  const auth = googleAuth(credential);
 | 
				
			||||||
  const integrationType = "google_calendar";
 | 
					  const integrationType = "google_calendar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -352,14 +366,16 @@ const GoogleCalendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
          const selectedCalendarIds = selectedCalendars
 | 
					          const selectedCalendarIds = selectedCalendars
 | 
				
			||||||
            .filter((e) => e.integration === integrationType)
 | 
					            .filter((e) => e.integration === integrationType)
 | 
				
			||||||
            .map((e) => e.externalId);
 | 
					            .map((e) => e.externalId);
 | 
				
			||||||
          if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0) {
 | 
					          if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
 | 
				
			||||||
            // Only calendars of other integrations selected
 | 
					            // Only calendars of other integrations selected
 | 
				
			||||||
            resolve([]);
 | 
					            resolve([]);
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          (selectedCalendarIds.length == 0
 | 
					          (selectedCalendarIds.length === 0
 | 
				
			||||||
            ? calendar.calendarList.list().then((cals) => cals.data.items.map((cal) => cal.id))
 | 
					            ? calendar.calendarList
 | 
				
			||||||
 | 
					                .list()
 | 
				
			||||||
 | 
					                .then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
 | 
				
			||||||
            : Promise.resolve(selectedCalendarIds)
 | 
					            : Promise.resolve(selectedCalendarIds)
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
            .then((calsIds) => {
 | 
					            .then((calsIds) => {
 | 
				
			||||||
| 
						 | 
					@ -375,6 +391,7 @@ const GoogleCalendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
                  if (err) {
 | 
					                  if (err) {
 | 
				
			||||||
                    reject(err);
 | 
					                    reject(err);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
 | 
					                  // @ts-ignore FIXME
 | 
				
			||||||
                  resolve(Object.values(apires.data.calendars).flatMap((item) => item["busy"]));
 | 
					                  resolve(Object.values(apires.data.calendars).flatMap((item) => item["busy"]));
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
| 
						 | 
					@ -388,7 +405,7 @@ const GoogleCalendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
    createEvent: (event: CalendarEvent) =>
 | 
					    createEvent: (event: CalendarEvent) =>
 | 
				
			||||||
      new Promise((resolve, reject) =>
 | 
					      new Promise((resolve, reject) =>
 | 
				
			||||||
        auth.getToken().then((myGoogleAuth) => {
 | 
					        auth.getToken().then((myGoogleAuth) => {
 | 
				
			||||||
          const payload = {
 | 
					          const payload: calendar_v3.Schema$Event = {
 | 
				
			||||||
            summary: event.title,
 | 
					            summary: event.title,
 | 
				
			||||||
            description: event.description,
 | 
					            description: event.description,
 | 
				
			||||||
            start: {
 | 
					            start: {
 | 
				
			||||||
| 
						 | 
					@ -422,14 +439,15 @@ const GoogleCalendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              auth: myGoogleAuth,
 | 
					              auth: myGoogleAuth,
 | 
				
			||||||
              calendarId: "primary",
 | 
					              calendarId: "primary",
 | 
				
			||||||
              resource: payload,
 | 
					              requestBody: payload,
 | 
				
			||||||
              conferenceDataVersion: 1,
 | 
					              conferenceDataVersion: 1,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            function (err, event) {
 | 
					            function (err, event) {
 | 
				
			||||||
              if (err) {
 | 
					              if (err || !event?.data) {
 | 
				
			||||||
                console.error("There was an error contacting google calendar service: ", err);
 | 
					                console.error("There was an error contacting google calendar service: ", err);
 | 
				
			||||||
                return reject(err);
 | 
					                return reject(err);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
 | 
					              // @ts-ignore FIXME
 | 
				
			||||||
              return resolve(event.data);
 | 
					              return resolve(event.data);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
| 
						 | 
					@ -438,7 +456,7 @@ const GoogleCalendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
    updateEvent: (uid: string, event: CalendarEvent) =>
 | 
					    updateEvent: (uid: string, event: CalendarEvent) =>
 | 
				
			||||||
      new Promise((resolve, reject) =>
 | 
					      new Promise((resolve, reject) =>
 | 
				
			||||||
        auth.getToken().then((myGoogleAuth) => {
 | 
					        auth.getToken().then((myGoogleAuth) => {
 | 
				
			||||||
          const payload = {
 | 
					          const payload: calendar_v3.Schema$Event = {
 | 
				
			||||||
            summary: event.title,
 | 
					            summary: event.title,
 | 
				
			||||||
            description: event.description,
 | 
					            description: event.description,
 | 
				
			||||||
            start: {
 | 
					            start: {
 | 
				
			||||||
| 
						 | 
					@ -471,14 +489,14 @@ const GoogleCalendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
              eventId: uid,
 | 
					              eventId: uid,
 | 
				
			||||||
              sendNotifications: true,
 | 
					              sendNotifications: true,
 | 
				
			||||||
              sendUpdates: "all",
 | 
					              sendUpdates: "all",
 | 
				
			||||||
              resource: payload,
 | 
					              requestBody: payload,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            function (err, event) {
 | 
					            function (err, event) {
 | 
				
			||||||
              if (err) {
 | 
					              if (err) {
 | 
				
			||||||
                console.error("There was an error contacting google calendar service: ", err);
 | 
					                console.error("There was an error contacting google calendar service: ", err);
 | 
				
			||||||
                return reject(err);
 | 
					                return reject(err);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              return resolve(event.data);
 | 
					              return resolve(event?.data);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
| 
						 | 
					@ -503,7 +521,7 @@ const GoogleCalendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
                console.error("There was an error contacting google calendar service: ", err);
 | 
					                console.error("There was an error contacting google calendar service: ", err);
 | 
				
			||||||
                return reject(err);
 | 
					                return reject(err);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              return resolve(event.data);
 | 
					              return resolve(event?.data);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
| 
						 | 
					@ -519,15 +537,15 @@ const GoogleCalendar = (credential): CalendarApiAdapter => {
 | 
				
			||||||
            .list()
 | 
					            .list()
 | 
				
			||||||
            .then((cals) => {
 | 
					            .then((cals) => {
 | 
				
			||||||
              resolve(
 | 
					              resolve(
 | 
				
			||||||
                cals.data.items.map((cal) => {
 | 
					                cals.data.items?.map((cal) => {
 | 
				
			||||||
                  const calendar: IntegrationCalendar = {
 | 
					                  const calendar: IntegrationCalendar = {
 | 
				
			||||||
                    externalId: cal.id,
 | 
					                    externalId: cal.id ?? "No id",
 | 
				
			||||||
                    integration: integrationType,
 | 
					                    integration: integrationType,
 | 
				
			||||||
                    name: cal.summary,
 | 
					                    name: cal.summary ?? "No name",
 | 
				
			||||||
                    primary: cal.primary,
 | 
					                    primary: cal.primary ?? false,
 | 
				
			||||||
                  };
 | 
					                  };
 | 
				
			||||||
                  return calendar;
 | 
					                  return calendar;
 | 
				
			||||||
                })
 | 
					                }) || []
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .catch((err) => {
 | 
					            .catch((err) => {
 | 
				
			||||||
| 
						 | 
					@ -576,7 +594,12 @@ const calendars = (withCredentials: Credential[]): CalendarApiAdapter[] =>
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .flatMap((item) => (item ? [item as CalendarApiAdapter] : []));
 | 
					    .flatMap((item) => (item ? [item as CalendarApiAdapter] : []));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) =>
 | 
					const getBusyCalendarTimes = (
 | 
				
			||||||
 | 
					  withCredentials: Credential[],
 | 
				
			||||||
 | 
					  dateFrom: string,
 | 
				
			||||||
 | 
					  dateTo: string,
 | 
				
			||||||
 | 
					  selectedCalendars: SelectedCalendar[]
 | 
				
			||||||
 | 
					) =>
 | 
				
			||||||
  Promise.all(
 | 
					  Promise.all(
 | 
				
			||||||
    calendars(withCredentials).map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars))
 | 
					    calendars(withCredentials).map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars))
 | 
				
			||||||
  ).then((results) => {
 | 
					  ).then((results) => {
 | 
				
			||||||
| 
						 | 
					@ -588,7 +611,7 @@ const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo, selectedCalenda
 | 
				
			||||||
 * @param withCredentials
 | 
					 * @param withCredentials
 | 
				
			||||||
 * @deprecated
 | 
					 * @deprecated
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const listCalendars = (withCredentials) =>
 | 
					const listCalendars = (withCredentials: Credential[]) =>
 | 
				
			||||||
  Promise.all(calendars(withCredentials).map((c) => c.listCalendars())).then((results) =>
 | 
					  Promise.all(calendars(withCredentials).map((c) => c.listCalendars())).then((results) =>
 | 
				
			||||||
    results.reduce((acc, calendars) => acc.concat(calendars), []).filter((c) => c != null)
 | 
					    results.reduce((acc, calendars) => acc.concat(calendars), []).filter((c) => c != null)
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
| 
						 | 
					@ -609,14 +632,15 @@ const createEvent = async (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let success = true;
 | 
					  let success = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const creationResult: any = credential
 | 
					  const creationResult = credential
 | 
				
			||||||
    ? await calendars([credential])[0]
 | 
					    ? await calendars([credential])[0]
 | 
				
			||||||
        .createEvent(richEvent)
 | 
					        .createEvent(richEvent)
 | 
				
			||||||
        .catch((e) => {
 | 
					        .catch((e) => {
 | 
				
			||||||
          log.error("createEvent failed", e, calEvent);
 | 
					          log.error("createEvent failed", e, calEvent);
 | 
				
			||||||
          success = false;
 | 
					          success = false;
 | 
				
			||||||
 | 
					          return undefined;
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    : null;
 | 
					    : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const metadata: AdditionInformation = {};
 | 
					  const metadata: AdditionInformation = {};
 | 
				
			||||||
  if (creationResult) {
 | 
					  if (creationResult) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,233 +0,0 @@
 | 
				
			||||||
import { Credential } from "@prisma/client";
 | 
					 | 
				
			||||||
import short from "short-uuid";
 | 
					 | 
				
			||||||
import { v5 as uuidv5 } from "uuid";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import CalEventParser from "@lib/CalEventParser";
 | 
					 | 
				
			||||||
import { getIntegrationName } from "@lib/emails/helpers";
 | 
					 | 
				
			||||||
import { EventResult } from "@lib/events/EventManager";
 | 
					 | 
				
			||||||
import logger from "@lib/logger";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { CalendarEvent, AdditionInformation, EntryPoint } from "./calendarClient";
 | 
					 | 
				
			||||||
import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail";
 | 
					 | 
				
			||||||
import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail";
 | 
					 | 
				
			||||||
import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail";
 | 
					 | 
				
			||||||
import VideoEventOrganizerMail from "./emails/VideoEventOrganizerMail";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const log = logger.getChildLogger({ prefix: ["[lib] dailyVideoClient"] });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const translator = short();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface DailyVideoCallData {
 | 
					 | 
				
			||||||
  type: string;
 | 
					 | 
				
			||||||
  id: string;
 | 
					 | 
				
			||||||
  password: string;
 | 
					 | 
				
			||||||
  url: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function handleErrorsJson(response: Response) {
 | 
					 | 
				
			||||||
  if (!response.ok) {
 | 
					 | 
				
			||||||
    response.json().then(console.log);
 | 
					 | 
				
			||||||
    throw Error(response.statusText);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return response.json();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const dailyCredential = process.env.DAILY_API_KEY;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface DailyVideoApiAdapter {
 | 
					 | 
				
			||||||
  dailyCreateMeeting(event: CalendarEvent): Promise<any>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  dailyUpdateMeeting(uid: string, event: CalendarEvent): Promise<any>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  dailyDeleteMeeting(uid: string): Promise<unknown>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getAvailability(dateFrom, dateTo): Promise<any>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const DailyVideo = (credential: Credential): DailyVideoApiAdapter => {
 | 
					 | 
				
			||||||
  const 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;
 | 
					 | 
				
			||||||
    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,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    getAvailability: () => {
 | 
					 | 
				
			||||||
      return credential;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    dailyCreateMeeting: (event: CalendarEvent) =>
 | 
					 | 
				
			||||||
      fetch("https://api.daily.co/v1/rooms", {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        headers: {
 | 
					 | 
				
			||||||
          Authorization: "Bearer " + dailyCredential,
 | 
					 | 
				
			||||||
          "Content-Type": "application/json",
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        body: JSON.stringify(translateEvent(event)),
 | 
					 | 
				
			||||||
      }).then(handleErrorsJson),
 | 
					 | 
				
			||||||
    dailyDeleteMeeting: (uid: string) =>
 | 
					 | 
				
			||||||
      fetch("https://api.daily.co/v1/rooms/" + uid, {
 | 
					 | 
				
			||||||
        method: "DELETE",
 | 
					 | 
				
			||||||
        headers: {
 | 
					 | 
				
			||||||
          Authorization: "Bearer " + dailyCredential,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      }).then(handleErrorsJson),
 | 
					 | 
				
			||||||
    dailyUpdateMeeting: (uid: string, event: CalendarEvent) =>
 | 
					 | 
				
			||||||
      fetch("https://api.daily.co/v1/rooms/" + uid, {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        headers: {
 | 
					 | 
				
			||||||
          Authorization: "Bearer " + dailyCredential,
 | 
					 | 
				
			||||||
          "Content-Type": "application/json",
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        body: JSON.stringify(translateEvent(event)),
 | 
					 | 
				
			||||||
      }).then(handleErrorsJson),
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// factory
 | 
					 | 
				
			||||||
const videoIntegrations = (withCredentials): DailyVideoApiAdapter[] =>
 | 
					 | 
				
			||||||
  withCredentials
 | 
					 | 
				
			||||||
    .map((cred) => {
 | 
					 | 
				
			||||||
      return DailyVideo(cred);
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .filter(Boolean);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const getBusyVideoTimes: (withCredentials) => Promise<unknown[]> = (withCredentials) =>
 | 
					 | 
				
			||||||
  Promise.all(videoIntegrations(withCredentials).map((c) => c.getAvailability())).then((results) =>
 | 
					 | 
				
			||||||
    results.reduce((acc, availability) => acc.concat(availability), [])
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const dailyCreateMeeting = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
 | 
					 | 
				
			||||||
  const parser: CalEventParser = new CalEventParser(calEvent);
 | 
					 | 
				
			||||||
  const uid: string = parser.getUid();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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."
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let success = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const creationResult = await videoIntegrations([credential])[0]
 | 
					 | 
				
			||||||
    .dailyCreateMeeting(calEvent)
 | 
					 | 
				
			||||||
    .catch((e) => {
 | 
					 | 
				
			||||||
      log.error("createMeeting failed", e, calEvent);
 | 
					 | 
				
			||||||
      success = false;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const currentRoute = process.env.BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const videoCallData: DailyVideoCallData = {
 | 
					 | 
				
			||||||
    type: "Daily.co Video",
 | 
					 | 
				
			||||||
    id: creationResult.name,
 | 
					 | 
				
			||||||
    password: creationResult.password,
 | 
					 | 
				
			||||||
    url: currentRoute + "/call/" + uid,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const entryPoint: EntryPoint = {
 | 
					 | 
				
			||||||
    entryPointType: getIntegrationName(videoCallData),
 | 
					 | 
				
			||||||
    uri: videoCallData.url,
 | 
					 | 
				
			||||||
    label: calEvent.language("enter_meeting"),
 | 
					 | 
				
			||||||
    pin: "",
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const additionInformation: AdditionInformation = {
 | 
					 | 
				
			||||||
    entryPoints: [entryPoint],
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
  const emailEvent = { ...calEvent, uid, additionInformation, videoCallData };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const organizerMail = new VideoEventOrganizerMail(emailEvent);
 | 
					 | 
				
			||||||
    await organizerMail.sendEmail();
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("organizerMail.sendEmail failed", e);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!creationResult || !creationResult.disableConfirmationEmail) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const attendeeMail = new VideoEventAttendeeMail(emailEvent);
 | 
					 | 
				
			||||||
      await attendeeMail.sendEmail();
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error("attendeeMail.sendEmail failed", e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    type: "daily",
 | 
					 | 
				
			||||||
    success,
 | 
					 | 
				
			||||||
    uid,
 | 
					 | 
				
			||||||
    createdEvent: creationResult,
 | 
					 | 
				
			||||||
    originalEvent: calEvent,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const dailyUpdateMeeting = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
 | 
					 | 
				
			||||||
  const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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."
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let success = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const updateResult =
 | 
					 | 
				
			||||||
    credential && calEvent.uid
 | 
					 | 
				
			||||||
      ? await videoIntegrations([credential])[0]
 | 
					 | 
				
			||||||
          .dailyUpdateMeeting(calEvent.uid, calEvent)
 | 
					 | 
				
			||||||
          .catch((e) => {
 | 
					 | 
				
			||||||
            log.error("updateMeeting failed", e, calEvent);
 | 
					 | 
				
			||||||
            success = false;
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
      : null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const emailEvent = { ...calEvent, uid: newUid };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const organizerMail = new EventOrganizerRescheduledMail(emailEvent);
 | 
					 | 
				
			||||||
    await organizerMail.sendEmail();
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("organizerMail.sendEmail failed", e);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!updateResult || !updateResult.disableConfirmationEmail) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const attendeeMail = new EventAttendeeRescheduledMail(emailEvent);
 | 
					 | 
				
			||||||
      await attendeeMail.sendEmail();
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error("attendeeMail.sendEmail failed", e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    type: credential.type,
 | 
					 | 
				
			||||||
    success,
 | 
					 | 
				
			||||||
    uid: newUid,
 | 
					 | 
				
			||||||
    updatedEvent: updateResult,
 | 
					 | 
				
			||||||
    originalEvent: calEvent,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const dailyDeleteMeeting = (credential: Credential, uid: string): Promise<unknown> => {
 | 
					 | 
				
			||||||
  if (credential) {
 | 
					 | 
				
			||||||
    return videoIntegrations([credential])[0].dailyDeleteMeeting(uid);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return Promise.resolve({});
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export { getBusyVideoTimes, dailyCreateMeeting, dailyUpdateMeeting, dailyDeleteMeeting };
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,9 @@
 | 
				
			||||||
export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: number; code?: unknown } {
 | 
					import { Prisma } from "@prisma/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: number; code?: string } {
 | 
				
			||||||
 | 
					  if (cause instanceof Prisma.PrismaClientKnownRequestError) {
 | 
				
			||||||
 | 
					    return cause;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  if (cause instanceof Error) {
 | 
					  if (cause instanceof Error) {
 | 
				
			||||||
    return cause;
 | 
					    return cause;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -9,3 +14,19 @@ export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: numb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return new Error(`Unhandled error of type '${typeof cause}''`);
 | 
					  return new Error(`Unhandled error of type '${typeof cause}''`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function handleErrorsJson(response: Response) {
 | 
				
			||||||
 | 
					  if (!response.ok) {
 | 
				
			||||||
 | 
					    response.json().then(console.log);
 | 
				
			||||||
 | 
					    throw Error(response.statusText);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return response.json();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function handleErrorsRaw(response: Response) {
 | 
				
			||||||
 | 
					  if (!response.ok) {
 | 
				
			||||||
 | 
					    response.text().then(console.log);
 | 
				
			||||||
 | 
					    throw Error(response.statusText);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return response.text();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,20 +3,27 @@ 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";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CalendarEvent, AdditionInformation, createEvent, updateEvent } from "@lib/calendarClient";
 | 
					import { AdditionInformation, CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient";
 | 
				
			||||||
import { dailyCreateMeeting, dailyUpdateMeeting } from "@lib/dailyVideoClient";
 | 
					 | 
				
			||||||
import EventAttendeeMail from "@lib/emails/EventAttendeeMail";
 | 
					import EventAttendeeMail from "@lib/emails/EventAttendeeMail";
 | 
				
			||||||
import EventAttendeeRescheduledMail from "@lib/emails/EventAttendeeRescheduledMail";
 | 
					import EventAttendeeRescheduledMail from "@lib/emails/EventAttendeeRescheduledMail";
 | 
				
			||||||
 | 
					import { DailyEventResult, FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
 | 
				
			||||||
 | 
					import { ZoomEventResult } from "@lib/integrations/Zoom/ZoomVideoApiAdapter";
 | 
				
			||||||
import { LocationType } from "@lib/location";
 | 
					import { LocationType } from "@lib/location";
 | 
				
			||||||
import prisma from "@lib/prisma";
 | 
					import prisma from "@lib/prisma";
 | 
				
			||||||
 | 
					import { Ensure } from "@lib/types/utils";
 | 
				
			||||||
import { createMeeting, updateMeeting, VideoCallData } from "@lib/videoClient";
 | 
					import { createMeeting, updateMeeting, VideoCallData } from "@lib/videoClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Event = AdditionInformation & { name: string; id: string; disableConfirmationEmail?: boolean } & (
 | 
				
			||||||
 | 
					    | ZoomEventResult
 | 
				
			||||||
 | 
					    | DailyEventResult
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface EventResult {
 | 
					export interface EventResult {
 | 
				
			||||||
  type: string;
 | 
					  type: string;
 | 
				
			||||||
  success: boolean;
 | 
					  success: boolean;
 | 
				
			||||||
  uid: string;
 | 
					  uid: string;
 | 
				
			||||||
  createdEvent?: AdditionInformation & { name: string; id: string; disableConfirmationEmail?: boolean };
 | 
					  createdEvent?: Event;
 | 
				
			||||||
  updatedEvent?: AdditionInformation & { name: string; id: string; disableConfirmationEmail?: boolean };
 | 
					  updatedEvent?: Event;
 | 
				
			||||||
  originalEvent: CalendarEvent;
 | 
					  originalEvent: CalendarEvent;
 | 
				
			||||||
  videoCallData?: VideoCallData;
 | 
					  videoCallData?: VideoCallData;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -44,9 +51,6 @@ interface GetLocationRequestFromIntegrationRequest {
 | 
				
			||||||
  location: string;
 | 
					  location: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//const to idenfity a daily event location
 | 
					 | 
				
			||||||
const dailyLocation = "integrations:daily";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default class EventManager {
 | 
					export default class EventManager {
 | 
				
			||||||
  calendarCredentials: Array<Credential>;
 | 
					  calendarCredentials: Array<Credential>;
 | 
				
			||||||
  videoCredentials: Array<Credential>;
 | 
					  videoCredentials: Array<Credential>;
 | 
				
			||||||
| 
						 | 
					@ -61,16 +65,9 @@ export default class EventManager {
 | 
				
			||||||
    this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video"));
 | 
					    this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //for  Daily.co video, temporarily pushes a credential for the daily-video-client
 | 
					    //for  Daily.co video, temporarily pushes a credential for the daily-video-client
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const hasDailyIntegration = process.env.DAILY_API_KEY;
 | 
					    const hasDailyIntegration = process.env.DAILY_API_KEY;
 | 
				
			||||||
    const dailyCredential: Credential = {
 | 
					 | 
				
			||||||
      id: +new Date().getTime(),
 | 
					 | 
				
			||||||
      type: "daily_video",
 | 
					 | 
				
			||||||
      key: { apikey: process.env.DAILY_API_KEY },
 | 
					 | 
				
			||||||
      userId: +new Date().getTime(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    if (hasDailyIntegration) {
 | 
					    if (hasDailyIntegration) {
 | 
				
			||||||
      this.videoCredentials.push(dailyCredential);
 | 
					      this.videoCredentials.push(FAKE_DAILY_CREDENTIAL);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -81,7 +78,7 @@ export default class EventManager {
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param event
 | 
					   * @param event
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public async create(event: CalendarEvent): Promise<CreateUpdateResult> {
 | 
					  public async create(event: Ensure<CalendarEvent, "language">): Promise<CreateUpdateResult> {
 | 
				
			||||||
    let evt = EventManager.processLocation(event);
 | 
					    let evt = EventManager.processLocation(event);
 | 
				
			||||||
    const isDedicated = evt.location ? EventManager.isDedicatedIntegration(evt.location) : null;
 | 
					    const isDedicated = evt.location ? EventManager.isDedicatedIntegration(evt.location) : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -103,13 +100,14 @@ export default class EventManager {
 | 
				
			||||||
    results = results.concat(await this.createAllCalendarEvents(evt, isDedicated));
 | 
					    results = results.concat(await this.createAllCalendarEvents(evt, isDedicated));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => {
 | 
					    const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => {
 | 
				
			||||||
      const isDailyResult = result.type === "daily";
 | 
					 | 
				
			||||||
      let uid = "";
 | 
					      let uid = "";
 | 
				
			||||||
      if (isDailyResult && result.createdEvent) {
 | 
					      if (result.createdEvent) {
 | 
				
			||||||
        uid = result.createdEvent.name.toString();
 | 
					        const isDailyResult = result.type === "daily_video";
 | 
				
			||||||
 | 
					        if (isDailyResult) {
 | 
				
			||||||
 | 
					          uid = (result.createdEvent as DailyEventResult).name.toString();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          uid = (result.createdEvent as ZoomEventResult).id.toString();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      if (!isDailyResult && result.createdEvent) {
 | 
					 | 
				
			||||||
        uid = result.createdEvent.id.toString();
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        type: result.type,
 | 
					        type: result.type,
 | 
				
			||||||
| 
						 | 
					@ -132,11 +130,11 @@ export default class EventManager {
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param event
 | 
					   * @param event
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public async update(event: CalendarEvent): Promise<CreateUpdateResult> {
 | 
					  public async update(event: Ensure<CalendarEvent, "uid">): Promise<CreateUpdateResult> {
 | 
				
			||||||
    let evt = EventManager.processLocation(event);
 | 
					    let evt = EventManager.processLocation(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!evt.uid) {
 | 
					    if (!evt.uid) {
 | 
				
			||||||
      throw new Error("missing uid");
 | 
					      throw new Error("You called eventManager.update without an `uid`. This should never happen.");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Get details of existing booking.
 | 
					    // Get details of existing booking.
 | 
				
			||||||
| 
						 | 
					@ -163,9 +161,7 @@ export default class EventManager {
 | 
				
			||||||
      throw new Error("booking not found");
 | 
					      throw new Error("booking not found");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const isDedicated = evt.location
 | 
					    const isDedicated = evt.location ? EventManager.isDedicatedIntegration(evt.location) : null;
 | 
				
			||||||
      ? EventManager.isDedicatedIntegration(evt.location) || evt.location === dailyLocation
 | 
					 | 
				
			||||||
      : null;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let results: Array<EventResult> = [];
 | 
					    let results: Array<EventResult> = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -259,15 +255,11 @@ export default class EventManager {
 | 
				
			||||||
   * @param event
 | 
					   * @param event
 | 
				
			||||||
   * @private
 | 
					   * @private
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private createVideoEvent(event: CalendarEvent): Promise<EventResult> {
 | 
					  private createVideoEvent(event: Ensure<CalendarEvent, "language">): Promise<EventResult> {
 | 
				
			||||||
    const credential = this.getVideoCredential(event);
 | 
					    const credential = this.getVideoCredential(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const isDaily = event.location === dailyLocation;
 | 
					    if (credential) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (credential && !isDaily) {
 | 
					 | 
				
			||||||
      return createMeeting(credential, event);
 | 
					      return createMeeting(credential, event);
 | 
				
			||||||
    } else if (credential && isDaily) {
 | 
					 | 
				
			||||||
      return dailyCreateMeeting(credential, event);
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return Promise.reject("No suitable credentials given for the requested integration name.");
 | 
					      return Promise.reject("No suitable credentials given for the requested integration name.");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -307,9 +299,8 @@ export default class EventManager {
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) {
 | 
					  private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) {
 | 
				
			||||||
    const credential = this.getVideoCredential(event);
 | 
					    const credential = this.getVideoCredential(event);
 | 
				
			||||||
    const isDaily = event.location === dailyLocation;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (credential && !isDaily) {
 | 
					    if (credential) {
 | 
				
			||||||
      const bookingRef = booking ? booking.references.filter((ref) => ref.type === credential.type)[0] : null;
 | 
					      const bookingRef = booking ? booking.references.filter((ref) => ref.type === credential.type)[0] : null;
 | 
				
			||||||
      const evt = { ...event, uid: bookingRef?.uid };
 | 
					      const evt = { ...event, uid: bookingRef?.uid };
 | 
				
			||||||
      return updateMeeting(credential, evt).then((returnVal: EventResult) => {
 | 
					      return updateMeeting(credential, evt).then((returnVal: EventResult) => {
 | 
				
			||||||
| 
						 | 
					@ -320,13 +311,6 @@ export default class EventManager {
 | 
				
			||||||
        return returnVal;
 | 
					        return returnVal;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      if (credential && isDaily) {
 | 
					 | 
				
			||||||
        const bookingRefUid = booking
 | 
					 | 
				
			||||||
          ? booking.references.filter((ref) => ref.type === "daily")[0].uid
 | 
					 | 
				
			||||||
          : null;
 | 
					 | 
				
			||||||
        const evt = { ...event, uid: bookingRefUid };
 | 
					 | 
				
			||||||
        return dailyUpdateMeeting(credential, evt);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return Promise.reject("No suitable credentials given for the requested integration name.");
 | 
					      return Promise.reject("No suitable credentials given for the requested integration name.");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -345,7 +329,7 @@ export default class EventManager {
 | 
				
			||||||
  private static isDedicatedIntegration(location: string): boolean {
 | 
					  private static isDedicatedIntegration(location: string): boolean {
 | 
				
			||||||
    // Hard-coded for now, because Zoom and Google Meet are both integrations, but one is dedicated, the other one isn't.
 | 
					    // Hard-coded for now, because Zoom and Google Meet are both integrations, but one is dedicated, the other one isn't.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return location === "integrations:zoom" || location === dailyLocation;
 | 
					    return location === "integrations:zoom" || location === "integrations:daily";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
| 
						 | 
					@ -385,7 +369,7 @@ export default class EventManager {
 | 
				
			||||||
   * @param event
 | 
					   * @param event
 | 
				
			||||||
   * @private
 | 
					   * @private
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private static processLocation(event: CalendarEvent): CalendarEvent {
 | 
					  private static processLocation<T extends CalendarEvent>(event: T): T {
 | 
				
			||||||
    // If location is set to an integration location
 | 
					    // If location is set to an integration location
 | 
				
			||||||
    // Build proper transforms for evt object
 | 
					    // Build proper transforms for evt object
 | 
				
			||||||
    // Extend evt object with those transformations
 | 
					    // Extend evt object with those transformations
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										131
									
								
								lib/integrations/Daily/DailyVideoApiAdapter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								lib/integrations/Daily/DailyVideoApiAdapter.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,131 @@
 | 
				
			||||||
 | 
					import { Credential } from "@prisma/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CalendarEvent } from "@lib/calendarClient";
 | 
				
			||||||
 | 
					import { handleErrorsJson } from "@lib/errors";
 | 
				
			||||||
 | 
					import prisma from "@lib/prisma";
 | 
				
			||||||
 | 
					import { VideoApiAdapter } from "@lib/videoClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DailyKey = {
 | 
				
			||||||
 | 
					  apikey: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const FAKE_DAILY_CREDENTIAL: Credential = {
 | 
				
			||||||
 | 
					  id: +new Date().getTime(),
 | 
				
			||||||
 | 
					  type: "daily_video",
 | 
				
			||||||
 | 
					  key: { apikey: process.env.DAILY_API_KEY },
 | 
				
			||||||
 | 
					  userId: +new Date().getTime(),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DailyVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
 | 
				
			||||||
 | 
					  const dailyApiToken = (credential.key as DailyKey).apikey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function postToDailyAPI(endpoint: string, body: Record<string, any>) {
 | 
				
			||||||
 | 
					    return fetch("https://api.daily.co/v1" + endpoint, {
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        Authorization: "Bearer " + dailyApiToken,
 | 
				
			||||||
 | 
					        "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      body: JSON.stringify(body),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent) {
 | 
				
			||||||
 | 
					    if (!event.uid) {
 | 
				
			||||||
 | 
					      throw new Error("We need need the booking uid to create the Daily reference in DB");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const response = await postToDailyAPI(endpoint, translateEvent(event));
 | 
				
			||||||
 | 
					    const dailyEvent: DailyReturnType = await handleErrorsJson(response);
 | 
				
			||||||
 | 
					    const res = await 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 dailyEvent;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const 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;
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    /** Daily doesn't need to return busy times, so we return empty */
 | 
				
			||||||
 | 
					    getAvailability: () => {
 | 
				
			||||||
 | 
					      return Promise.resolve([]);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createMeeting: async (event: CalendarEvent) => createOrUpdateMeeting("/rooms", event),
 | 
				
			||||||
 | 
					    deleteMeeting: (uid: string) =>
 | 
				
			||||||
 | 
					      fetch("https://api.daily.co/v1/rooms/" + uid, {
 | 
				
			||||||
 | 
					        method: "DELETE",
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					          Authorization: "Bearer " + dailyApiToken,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }).then(handleErrorsJson),
 | 
				
			||||||
 | 
					    updateMeeting: (uid: string, event: CalendarEvent) => createOrUpdateMeeting("/rooms/" + uid, event),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default DailyVideoApiAdapter;
 | 
				
			||||||
							
								
								
									
										205
									
								
								lib/integrations/Zoom/ZoomVideoApiAdapter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								lib/integrations/Zoom/ZoomVideoApiAdapter.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,205 @@
 | 
				
			||||||
 | 
					import { Credential } from "@prisma/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CalendarEvent } from "@lib/calendarClient";
 | 
				
			||||||
 | 
					import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
 | 
				
			||||||
 | 
					import prisma from "@lib/prisma";
 | 
				
			||||||
 | 
					import { VideoApiAdapter } from "@lib/videoClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @link https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate */
 | 
				
			||||||
 | 
					export interface ZoomEventResult {
 | 
				
			||||||
 | 
					  created_at: string;
 | 
				
			||||||
 | 
					  duration: number;
 | 
				
			||||||
 | 
					  host_id: string;
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  join_url: string;
 | 
				
			||||||
 | 
					  settings: {
 | 
				
			||||||
 | 
					    alternative_hosts: string;
 | 
				
			||||||
 | 
					    approval_type: number;
 | 
				
			||||||
 | 
					    audio: string;
 | 
				
			||||||
 | 
					    auto_recording: string;
 | 
				
			||||||
 | 
					    close_registration: boolean;
 | 
				
			||||||
 | 
					    cn_meeting: boolean;
 | 
				
			||||||
 | 
					    enforce_login: boolean;
 | 
				
			||||||
 | 
					    enforce_login_domains: string;
 | 
				
			||||||
 | 
					    global_dial_in_countries: string[];
 | 
				
			||||||
 | 
					    global_dial_in_numbers: {
 | 
				
			||||||
 | 
					      city: string;
 | 
				
			||||||
 | 
					      country: string;
 | 
				
			||||||
 | 
					      country_name: string;
 | 
				
			||||||
 | 
					      number: string;
 | 
				
			||||||
 | 
					      type: string;
 | 
				
			||||||
 | 
					    }[];
 | 
				
			||||||
 | 
					    breakout_room: {
 | 
				
			||||||
 | 
					      enable: boolean;
 | 
				
			||||||
 | 
					      rooms: {
 | 
				
			||||||
 | 
					        name: string;
 | 
				
			||||||
 | 
					        participants: string[];
 | 
				
			||||||
 | 
					      }[];
 | 
				
			||||||
 | 
					      host_video: boolean;
 | 
				
			||||||
 | 
					      in_meeting: boolean;
 | 
				
			||||||
 | 
					      join_before_host: boolean;
 | 
				
			||||||
 | 
					      mute_upon_entry: boolean;
 | 
				
			||||||
 | 
					      participant_video: boolean;
 | 
				
			||||||
 | 
					      registrants_confirmation_email: boolean;
 | 
				
			||||||
 | 
					      use_pmi: boolean;
 | 
				
			||||||
 | 
					      waiting_room: boolean;
 | 
				
			||||||
 | 
					      watermark: boolean;
 | 
				
			||||||
 | 
					      registrants_email_notification: boolean;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    start_time: string;
 | 
				
			||||||
 | 
					    start_url: string;
 | 
				
			||||||
 | 
					    status: string;
 | 
				
			||||||
 | 
					    timezone: string;
 | 
				
			||||||
 | 
					    topic: string;
 | 
				
			||||||
 | 
					    type: number;
 | 
				
			||||||
 | 
					    uuid: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ZoomToken {
 | 
				
			||||||
 | 
					  scope: "meeting:write";
 | 
				
			||||||
 | 
					  expires_in: number;
 | 
				
			||||||
 | 
					  token_type: "bearer";
 | 
				
			||||||
 | 
					  access_token: string;
 | 
				
			||||||
 | 
					  refresh_token: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const zoomAuth = (credential: Credential) => {
 | 
				
			||||||
 | 
					  const credentialKey = credential.key as unknown as ZoomToken;
 | 
				
			||||||
 | 
					  const isExpired = (expiryDate: number) => expiryDate < +new Date();
 | 
				
			||||||
 | 
					  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) => {
 | 
				
			||||||
 | 
					        // Store new tokens in database.
 | 
				
			||||||
 | 
					        await prisma.credential.update({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            id: credential.id,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            key: responseBody,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        credentialKey.access_token = responseBody.access_token;
 | 
				
			||||||
 | 
					        credentialKey.expires_in = Math.round(+new Date() / 1000 + responseBody.expires_in);
 | 
				
			||||||
 | 
					        return credentialKey.access_token;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    getToken: () =>
 | 
				
			||||||
 | 
					      !isExpired(credentialKey.expires_in)
 | 
				
			||||||
 | 
					        ? Promise.resolve(credentialKey.access_token)
 | 
				
			||||||
 | 
					        : refreshAccessToken(credentialKey.refresh_token),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
 | 
				
			||||||
 | 
					  const auth = zoomAuth(credential);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    getAvailability: () => {
 | 
				
			||||||
 | 
					      return 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 [];
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createMeeting: (event: CalendarEvent) =>
 | 
				
			||||||
 | 
					      auth.getToken().then((accessToken) =>
 | 
				
			||||||
 | 
					        fetch("https://api.zoom.us/v2/users/me/meetings", {
 | 
				
			||||||
 | 
					          method: "POST",
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            Authorization: "Bearer " + accessToken,
 | 
				
			||||||
 | 
					            "Content-Type": "application/json",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          body: JSON.stringify(translateEvent(event)),
 | 
				
			||||||
 | 
					        }).then(handleErrorsJson)
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    deleteMeeting: (uid: string) =>
 | 
				
			||||||
 | 
					      auth.getToken().then((accessToken) =>
 | 
				
			||||||
 | 
					        fetch("https://api.zoom.us/v2/meetings/" + uid, {
 | 
				
			||||||
 | 
					          method: "DELETE",
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            Authorization: "Bearer " + accessToken,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }).then(handleErrorsRaw)
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    updateMeeting: (uid: string, event: CalendarEvent) =>
 | 
				
			||||||
 | 
					      auth.getToken().then((accessToken: string) =>
 | 
				
			||||||
 | 
					        fetch("https://api.zoom.us/v2/meetings/" + uid, {
 | 
				
			||||||
 | 
					          method: "PATCH",
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            Authorization: "Bearer " + accessToken,
 | 
				
			||||||
 | 
					            "Content-Type": "application/json",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          body: JSON.stringify(translateEvent(event)),
 | 
				
			||||||
 | 
					        }).then(handleErrorsRaw)
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ZoomVideoApiAdapter;
 | 
				
			||||||
| 
						 | 
					@ -2,12 +2,17 @@ import { Booking } from "@prisma/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { LocationType } from "@lib/location";
 | 
					import { LocationType } from "@lib/location";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type BookingConfirmBody = {
 | 
				
			||||||
 | 
					  confirmed: boolean;
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type BookingCreateBody = {
 | 
					export type BookingCreateBody = {
 | 
				
			||||||
  email: string;
 | 
					  email: string;
 | 
				
			||||||
  end: string;
 | 
					  end: string;
 | 
				
			||||||
  eventTypeId: number;
 | 
					  eventTypeId: number;
 | 
				
			||||||
  guests: string[];
 | 
					  guests: string[];
 | 
				
			||||||
  location?: LocationType;
 | 
					  location: LocationType;
 | 
				
			||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
  notes: string;
 | 
					  notes: string;
 | 
				
			||||||
  rescheduleUid?: string;
 | 
					  rescheduleUid?: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,3 @@
 | 
				
			||||||
export type RequiredNotNull<T> = {
 | 
					export type Ensure<T, K extends keyof T> = Omit<T, K> & {
 | 
				
			||||||
  [P in keyof T]: NonNullable<T[P]>;
 | 
					  [EK in K]-?: NonNullable<T[EK]>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					 | 
				
			||||||
export type Ensure<T, K extends keyof T> = T & RequiredNotNull<Pick<T, K>>;
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,29 +3,24 @@ import short from "short-uuid";
 | 
				
			||||||
import { v5 as uuidv5 } from "uuid";
 | 
					import { v5 as uuidv5 } from "uuid";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import CalEventParser from "@lib/CalEventParser";
 | 
					import CalEventParser from "@lib/CalEventParser";
 | 
				
			||||||
 | 
					import "@lib/emails/EventMail";
 | 
				
			||||||
import { getIntegrationName } from "@lib/emails/helpers";
 | 
					import { getIntegrationName } from "@lib/emails/helpers";
 | 
				
			||||||
import { EventResult } from "@lib/events/EventManager";
 | 
					import { EventResult } from "@lib/events/EventManager";
 | 
				
			||||||
import logger from "@lib/logger";
 | 
					import logger from "@lib/logger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CalendarEvent, AdditionInformation, EntryPoint } from "./calendarClient";
 | 
					import { AdditionInformation, CalendarEvent, EntryPoint } from "./calendarClient";
 | 
				
			||||||
import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail";
 | 
					import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail";
 | 
				
			||||||
import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail";
 | 
					import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail";
 | 
				
			||||||
import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail";
 | 
					import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail";
 | 
				
			||||||
import VideoEventOrganizerMail from "./emails/VideoEventOrganizerMail";
 | 
					import VideoEventOrganizerMail from "./emails/VideoEventOrganizerMail";
 | 
				
			||||||
import prisma from "./prisma";
 | 
					import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
 | 
				
			||||||
 | 
					import ZoomVideoApiAdapter from "./integrations/Zoom/ZoomVideoApiAdapter";
 | 
				
			||||||
 | 
					import { Ensure } from "./types/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
 | 
					const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const translator = short();
 | 
					const translator = short();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ZoomToken {
 | 
					 | 
				
			||||||
  scope: "meeting:write";
 | 
					 | 
				
			||||||
  expires_in: number;
 | 
					 | 
				
			||||||
  token_type: "bearer";
 | 
					 | 
				
			||||||
  access_token: string;
 | 
					 | 
				
			||||||
  refresh_token: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface VideoCallData {
 | 
					export interface VideoCallData {
 | 
				
			||||||
  type: string;
 | 
					  type: string;
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
| 
						 | 
					@ -33,176 +28,27 @@ export interface VideoCallData {
 | 
				
			||||||
  url: string;
 | 
					  url: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function handleErrorsJson(response: Response) {
 | 
					type EventBusyDate = Record<"start" | "end", Date>;
 | 
				
			||||||
  if (!response.ok) {
 | 
					 | 
				
			||||||
    response.json().then(console.log);
 | 
					 | 
				
			||||||
    throw Error(response.statusText);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return response.json();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function handleErrorsRaw(response: Response) {
 | 
					export interface VideoApiAdapter {
 | 
				
			||||||
  if (!response.ok) {
 | 
					 | 
				
			||||||
    response.text().then(console.log);
 | 
					 | 
				
			||||||
    throw Error(response.statusText);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return response.text();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const zoomAuth = (credential: Credential) => {
 | 
					 | 
				
			||||||
  const credentialKey = credential.key as unknown as ZoomToken;
 | 
					 | 
				
			||||||
  const isExpired = (expiryDate: number) => expiryDate < +new Date();
 | 
					 | 
				
			||||||
  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) => {
 | 
					 | 
				
			||||||
        // Store new tokens in database.
 | 
					 | 
				
			||||||
        await prisma.credential.update({
 | 
					 | 
				
			||||||
          where: {
 | 
					 | 
				
			||||||
            id: credential.id,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          data: {
 | 
					 | 
				
			||||||
            key: responseBody,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        credentialKey.access_token = responseBody.access_token;
 | 
					 | 
				
			||||||
        credentialKey.expires_in = Math.round(+new Date() / 1000 + responseBody.expires_in);
 | 
					 | 
				
			||||||
        return credentialKey.access_token;
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    getToken: () =>
 | 
					 | 
				
			||||||
      !isExpired(credentialKey.expires_in)
 | 
					 | 
				
			||||||
        ? Promise.resolve(credentialKey.access_token)
 | 
					 | 
				
			||||||
        : refreshAccessToken(credentialKey.refresh_token),
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface VideoApiAdapter {
 | 
					 | 
				
			||||||
  createMeeting(event: CalendarEvent): Promise<any>;
 | 
					  createMeeting(event: CalendarEvent): Promise<any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateMeeting(uid: string, event: CalendarEvent): Promise<any>;
 | 
					  updateMeeting(uid: string, event: CalendarEvent): Promise<any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  deleteMeeting(uid: string): Promise<unknown>;
 | 
					  deleteMeeting(uid: string): Promise<unknown>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAvailability(dateFrom: string, dateTo: string): Promise<any>;
 | 
					  getAvailability(dateFrom?: string, dateTo?: string): Promise<EventBusyDate[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ZoomVideo = (credential: Credential): VideoApiAdapter => {
 | 
					 | 
				
			||||||
  const auth = zoomAuth(credential);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    getAvailability: () => {
 | 
					 | 
				
			||||||
      return 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: 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: (event: CalendarEvent) =>
 | 
					 | 
				
			||||||
      auth.getToken().then((accessToken) =>
 | 
					 | 
				
			||||||
        fetch("https://api.zoom.us/v2/users/me/meetings", {
 | 
					 | 
				
			||||||
          method: "POST",
 | 
					 | 
				
			||||||
          headers: {
 | 
					 | 
				
			||||||
            Authorization: "Bearer " + accessToken,
 | 
					 | 
				
			||||||
            "Content-Type": "application/json",
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          body: JSON.stringify(translateEvent(event)),
 | 
					 | 
				
			||||||
        }).then(handleErrorsJson)
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    deleteMeeting: (uid: string) =>
 | 
					 | 
				
			||||||
      auth.getToken().then((accessToken) =>
 | 
					 | 
				
			||||||
        fetch("https://api.zoom.us/v2/meetings/" + uid, {
 | 
					 | 
				
			||||||
          method: "DELETE",
 | 
					 | 
				
			||||||
          headers: {
 | 
					 | 
				
			||||||
            Authorization: "Bearer " + accessToken,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        }).then(handleErrorsRaw)
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    updateMeeting: (uid: string, event: CalendarEvent) =>
 | 
					 | 
				
			||||||
      auth.getToken().then((accessToken) =>
 | 
					 | 
				
			||||||
        fetch("https://api.zoom.us/v2/meetings/" + uid, {
 | 
					 | 
				
			||||||
          method: "PATCH",
 | 
					 | 
				
			||||||
          headers: {
 | 
					 | 
				
			||||||
            Authorization: "Bearer " + accessToken,
 | 
					 | 
				
			||||||
            "Content-Type": "application/json",
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          body: JSON.stringify(translateEvent(event)),
 | 
					 | 
				
			||||||
        }).then(handleErrorsRaw)
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// factory
 | 
					// factory
 | 
				
			||||||
const videoIntegrations = (withCredentials: Credential[]): VideoApiAdapter[] =>
 | 
					const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] =>
 | 
				
			||||||
  withCredentials.reduce<VideoApiAdapter[]>((acc, cred) => {
 | 
					  withCredentials.reduce<VideoApiAdapter[]>((acc, cred) => {
 | 
				
			||||||
    switch (cred.type) {
 | 
					    switch (cred.type) {
 | 
				
			||||||
      case "zoom_video":
 | 
					      case "zoom_video":
 | 
				
			||||||
        acc.push(ZoomVideo(cred));
 | 
					        acc.push(ZoomVideoApiAdapter(cred));
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case "daily_video":
 | 
				
			||||||
 | 
					        acc.push(DailyVideoApiAdapter(cred));
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
| 
						 | 
					@ -211,11 +57,14 @@ const videoIntegrations = (withCredentials: Credential[]): VideoApiAdapter[] =>
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getBusyVideoTimes: (withCredentials: Credential[]) => Promise<unknown[]> = (withCredentials) =>
 | 
					const getBusyVideoTimes: (withCredentials: Credential[]) => Promise<unknown[]> = (withCredentials) =>
 | 
				
			||||||
  Promise.all(videoIntegrations(withCredentials).map((c) => c.getAvailability())).then((results) =>
 | 
					  Promise.all(getVideoAdapters(withCredentials).map((c) => c.getAvailability())).then((results) =>
 | 
				
			||||||
    results.reduce((acc, availability) => acc.concat(availability), [])
 | 
					    results.reduce((acc, availability) => acc.concat(availability), [])
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createMeeting = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
 | 
					const createMeeting = async (
 | 
				
			||||||
 | 
					  credential: Credential,
 | 
				
			||||||
 | 
					  calEvent: Ensure<CalendarEvent, "language">
 | 
				
			||||||
 | 
					): Promise<EventResult> => {
 | 
				
			||||||
  const parser: CalEventParser = new CalEventParser(calEvent);
 | 
					  const parser: CalEventParser = new CalEventParser(calEvent);
 | 
				
			||||||
  const uid: string = parser.getUid();
 | 
					  const uid: string = parser.getUid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -227,20 +76,35 @@ const createMeeting = async (credential: Credential, calEvent: CalendarEvent): P
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let success = true;
 | 
					  let success = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const creationResult = await videoIntegrations([credential])[0]
 | 
					  const videoAdapters = getVideoAdapters([credential]);
 | 
				
			||||||
    .createMeeting(calEvent)
 | 
					  const [firstVideoAdapter] = videoAdapters;
 | 
				
			||||||
    .catch((e) => {
 | 
					  const createdMeeting = await firstVideoAdapter.createMeeting(calEvent).catch((e) => {
 | 
				
			||||||
    log.error("createMeeting failed", e, calEvent);
 | 
					    log.error("createMeeting failed", e, calEvent);
 | 
				
			||||||
    success = false;
 | 
					    success = false;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!createdMeeting) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: credential.type,
 | 
				
			||||||
 | 
					      success,
 | 
				
			||||||
 | 
					      uid,
 | 
				
			||||||
 | 
					      originalEvent: calEvent,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const videoCallData: VideoCallData = {
 | 
					  const videoCallData: VideoCallData = {
 | 
				
			||||||
    type: credential.type,
 | 
					    type: credential.type,
 | 
				
			||||||
    id: creationResult.id,
 | 
					    id: createdMeeting.id,
 | 
				
			||||||
    password: creationResult.password,
 | 
					    password: createdMeeting.password,
 | 
				
			||||||
    url: creationResult.join_url,
 | 
					    url: createdMeeting.join_url,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (credential.type === "daily_video") {
 | 
				
			||||||
 | 
					    videoCallData.type = "Daily.co Video";
 | 
				
			||||||
 | 
					    videoCallData.id = createdMeeting.name;
 | 
				
			||||||
 | 
					    videoCallData.url = process.env.BASE_URL + "/call/" + uid;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const entryPoint: EntryPoint = {
 | 
					  const entryPoint: EntryPoint = {
 | 
				
			||||||
    entryPointType: getIntegrationName(videoCallData),
 | 
					    entryPointType: getIntegrationName(videoCallData),
 | 
				
			||||||
    uri: videoCallData.url,
 | 
					    uri: videoCallData.url,
 | 
				
			||||||
| 
						 | 
					@ -261,7 +125,7 @@ const createMeeting = async (credential: Credential, calEvent: CalendarEvent): P
 | 
				
			||||||
    console.error("organizerMail.sendEmail failed", e);
 | 
					    console.error("organizerMail.sendEmail failed", e);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!creationResult || !creationResult.disableConfirmationEmail) {
 | 
					  if (!createdMeeting || !createdMeeting.disableConfirmationEmail) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const attendeeMail = new VideoEventAttendeeMail(emailEvent);
 | 
					      const attendeeMail = new VideoEventAttendeeMail(emailEvent);
 | 
				
			||||||
      await attendeeMail.sendEmail();
 | 
					      await attendeeMail.sendEmail();
 | 
				
			||||||
| 
						 | 
					@ -274,7 +138,7 @@ const createMeeting = async (credential: Credential, calEvent: CalendarEvent): P
 | 
				
			||||||
    type: credential.type,
 | 
					    type: credential.type,
 | 
				
			||||||
    success,
 | 
					    success,
 | 
				
			||||||
    uid,
 | 
					    uid,
 | 
				
			||||||
    createdEvent: creationResult,
 | 
					    createdEvent: createdMeeting,
 | 
				
			||||||
    originalEvent: calEvent,
 | 
					    originalEvent: calEvent,
 | 
				
			||||||
    videoCallData: videoCallData,
 | 
					    videoCallData: videoCallData,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					@ -289,17 +153,26 @@ const updateMeeting = async (credential: Credential, calEvent: CalendarEvent): P
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!calEvent.uid) {
 | 
				
			||||||
 | 
					    throw new Error("You can't update an meeting without it's UID.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let success = true;
 | 
					  let success = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const updateResult =
 | 
					  const [firstVideoAdapter] = getVideoAdapters([credential]);
 | 
				
			||||||
    credential && calEvent.uid
 | 
					  const updatedMeeting = await firstVideoAdapter.updateMeeting(calEvent.uid, calEvent).catch((e) => {
 | 
				
			||||||
      ? await videoIntegrations([credential])[0]
 | 
					 | 
				
			||||||
          .updateMeeting(calEvent.uid, calEvent)
 | 
					 | 
				
			||||||
          .catch((e) => {
 | 
					 | 
				
			||||||
    log.error("updateMeeting failed", e, calEvent);
 | 
					    log.error("updateMeeting failed", e, calEvent);
 | 
				
			||||||
    success = false;
 | 
					    success = false;
 | 
				
			||||||
          })
 | 
					  });
 | 
				
			||||||
      : null;
 | 
					
 | 
				
			||||||
 | 
					  if (!updatedMeeting) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: credential.type,
 | 
				
			||||||
 | 
					      success,
 | 
				
			||||||
 | 
					      uid: calEvent.uid,
 | 
				
			||||||
 | 
					      originalEvent: calEvent,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const emailEvent = { ...calEvent, uid: newUid };
 | 
					  const emailEvent = { ...calEvent, uid: newUid };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -310,7 +183,7 @@ const updateMeeting = async (credential: Credential, calEvent: CalendarEvent): P
 | 
				
			||||||
    console.error("organizerMail.sendEmail failed", e);
 | 
					    console.error("organizerMail.sendEmail failed", e);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!updateResult || !updateResult.disableConfirmationEmail) {
 | 
					  if (!updatedMeeting.disableConfirmationEmail) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const attendeeMail = new EventAttendeeRescheduledMail(emailEvent);
 | 
					      const attendeeMail = new EventAttendeeRescheduledMail(emailEvent);
 | 
				
			||||||
      await attendeeMail.sendEmail();
 | 
					      await attendeeMail.sendEmail();
 | 
				
			||||||
| 
						 | 
					@ -323,14 +196,14 @@ const updateMeeting = async (credential: Credential, calEvent: CalendarEvent): P
 | 
				
			||||||
    type: credential.type,
 | 
					    type: credential.type,
 | 
				
			||||||
    success,
 | 
					    success,
 | 
				
			||||||
    uid: newUid,
 | 
					    uid: newUid,
 | 
				
			||||||
    updatedEvent: updateResult,
 | 
					    updatedEvent: updatedMeeting,
 | 
				
			||||||
    originalEvent: calEvent,
 | 
					    originalEvent: calEvent,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const deleteMeeting = (credential: Credential, uid: string): Promise<unknown> => {
 | 
					const deleteMeeting = (credential: Credential, uid: string): Promise<unknown> => {
 | 
				
			||||||
  if (credential) {
 | 
					  if (credential) {
 | 
				
			||||||
    return videoIntegrations([credential])[0].deleteMeeting(uid);
 | 
					    return getVideoAdapters([credential])[0].deleteMeeting(uid);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return Promise.resolve({});
 | 
					  return Promise.resolve({});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,6 +97,7 @@
 | 
				
			||||||
    "zod": "^3.8.2"
 | 
					    "zod": "^3.8.2"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
 | 
				
			||||||
    "@trivago/prettier-plugin-sort-imports": "2.0.4",
 | 
					    "@trivago/prettier-plugin-sort-imports": "2.0.4",
 | 
				
			||||||
    "@types/accept-language-parser": "1.5.2",
 | 
					    "@types/accept-language-parser": "1.5.2",
 | 
				
			||||||
    "@types/async": "^3.2.7",
 | 
					    "@types/async": "^3.2.7",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
// import { getBusyVideoTimes } from "@lib/videoClient";
 | 
					// import { getBusyVideoTimes } from "@lib/videoClient";
 | 
				
			||||||
 | 
					import { Prisma } from "@prisma/client";
 | 
				
			||||||
import dayjs from "dayjs";
 | 
					import dayjs from "dayjs";
 | 
				
			||||||
import type { NextApiRequest, NextApiResponse } from "next";
 | 
					import type { NextApiRequest, NextApiResponse } from "next";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,8 +7,6 @@ import { asStringOrNull } from "@lib/asStringOrNull";
 | 
				
			||||||
import { getBusyCalendarTimes } from "@lib/calendarClient";
 | 
					import { getBusyCalendarTimes } from "@lib/calendarClient";
 | 
				
			||||||
import prisma from "@lib/prisma";
 | 
					import prisma from "@lib/prisma";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Prisma } from ".prisma/client";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
 | 
					export default async function handler(req: NextApiRequest, res: NextApiResponse) {
 | 
				
			||||||
  const user = asStringOrNull(req.query.user);
 | 
					  const user = asStringOrNull(req.query.user);
 | 
				
			||||||
  const dateFrom = dayjs(asStringOrNull(req.query.dateFrom));
 | 
					  const dateFrom = dayjs(asStringOrNull(req.query.dateFrom));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ import { CalendarEvent } from "@lib/calendarClient";
 | 
				
			||||||
import EventRejectionMail from "@lib/emails/EventRejectionMail";
 | 
					import EventRejectionMail from "@lib/emails/EventRejectionMail";
 | 
				
			||||||
import EventManager from "@lib/events/EventManager";
 | 
					import EventManager from "@lib/events/EventManager";
 | 
				
			||||||
import prisma from "@lib/prisma";
 | 
					import prisma from "@lib/prisma";
 | 
				
			||||||
 | 
					import { BookingConfirmBody } from "@lib/types/booking";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { getTranslation } from "@server/lib/i18n";
 | 
					import { getTranslation } from "@server/lib/i18n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +19,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
    return res.status(401).json({ message: "Not authenticated" });
 | 
					    return res.status(401).json({ message: "Not authenticated" });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const bookingId = req.body.id;
 | 
					  const reqBody = req.body as BookingConfirmBody;
 | 
				
			||||||
 | 
					  const bookingId = reqBody.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!bookingId) {
 | 
					  if (!bookingId) {
 | 
				
			||||||
    return res.status(400).json({ message: "bookingId missing" });
 | 
					    return res.status(400).json({ message: "bookingId missing" });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -60,7 +63,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!booking || booking.userId != currentUser.id) {
 | 
					    if (!booking || booking.userId !== currentUser.id) {
 | 
				
			||||||
      return res.status(404).json({ message: "booking not found" });
 | 
					      return res.status(404).json({ message: "booking not found" });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (booking.confirmed) {
 | 
					    if (booking.confirmed) {
 | 
				
			||||||
| 
						 | 
					@ -75,12 +78,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
      endTime: booking.endTime.toISOString(),
 | 
					      endTime: booking.endTime.toISOString(),
 | 
				
			||||||
      organizer: { email: currentUser.email, name: currentUser.name!, timeZone: currentUser.timeZone },
 | 
					      organizer: { email: currentUser.email, name: currentUser.name!, timeZone: currentUser.timeZone },
 | 
				
			||||||
      attendees: booking.attendees,
 | 
					      attendees: booking.attendees,
 | 
				
			||||||
      location: booking.location,
 | 
					      location: booking.location ?? "",
 | 
				
			||||||
      uid: booking.uid,
 | 
					      uid: booking.uid,
 | 
				
			||||||
      language: t,
 | 
					      language: t,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (req.body.confirmed) {
 | 
					    if (reqBody.confirmed) {
 | 
				
			||||||
      const eventManager = new EventManager(currentUser.credentials);
 | 
					      const eventManager = new EventManager(currentUser.credentials);
 | 
				
			||||||
      const scheduleResult = await eventManager.create(evt);
 | 
					      const scheduleResult = await eventManager.create(evt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ import { CalendarEvent, getBusyCalendarTimes } from "@lib/calendarClient";
 | 
				
			||||||
import EventOrganizerRequestMail from "@lib/emails/EventOrganizerRequestMail";
 | 
					import EventOrganizerRequestMail from "@lib/emails/EventOrganizerRequestMail";
 | 
				
			||||||
import { getErrorFromUnknown } from "@lib/errors";
 | 
					import { getErrorFromUnknown } from "@lib/errors";
 | 
				
			||||||
import { getEventName } from "@lib/event";
 | 
					import { getEventName } from "@lib/event";
 | 
				
			||||||
import EventManager, { CreateUpdateResult, EventResult, PartialReference } from "@lib/events/EventManager";
 | 
					import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
 | 
				
			||||||
import logger from "@lib/logger";
 | 
					import logger from "@lib/logger";
 | 
				
			||||||
import prisma from "@lib/prisma";
 | 
					import prisma from "@lib/prisma";
 | 
				
			||||||
import { BookingCreateBody } from "@lib/types/booking";
 | 
					import { BookingCreateBody } from "@lib/types/booking";
 | 
				
			||||||
| 
						 | 
					@ -329,6 +329,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
  let booking: Booking | null = null;
 | 
					  let booking: Booking | null = null;
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    booking = await createBooking();
 | 
					    booking = await createBooking();
 | 
				
			||||||
 | 
					    evt.uid = booking.uid;
 | 
				
			||||||
  } catch (_err) {
 | 
					  } catch (_err) {
 | 
				
			||||||
    const err = getErrorFromUnknown(_err);
 | 
					    const err = getErrorFromUnknown(_err);
 | 
				
			||||||
    log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
 | 
					    log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
 | 
				
			||||||
| 
						 | 
					@ -431,7 +432,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
  if (rescheduleUid) {
 | 
					  if (rescheduleUid) {
 | 
				
			||||||
    // Use EventManager to conditionally use all needed integrations.
 | 
					    // Use EventManager to conditionally use all needed integrations.
 | 
				
			||||||
    const eventManagerCalendarEvent = { ...evt, uid: rescheduleUid };
 | 
					    const eventManagerCalendarEvent = { ...evt, uid: rescheduleUid };
 | 
				
			||||||
    const updateResults: CreateUpdateResult = await eventManager.update(eventManagerCalendarEvent);
 | 
					    const updateResults = await eventManager.update(eventManagerCalendarEvent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    results = updateResults.results;
 | 
					    results = updateResults.results;
 | 
				
			||||||
    referencesToCreate = updateResults.referencesToCreate;
 | 
					    referencesToCreate = updateResults.referencesToCreate;
 | 
				
			||||||
| 
						 | 
					@ -444,9 +445,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      log.error(`Booking ${user.name} failed`, error, results);
 | 
					      log.error(`Booking ${user.name} failed`, error, results);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // If it's not a reschedule, doesn't require confirmation and there's no price,
 | 
				
			||||||
 | 
					    // Create a booking
 | 
				
			||||||
  } else if (!eventType.requiresConfirmation && !eventType.price) {
 | 
					  } else if (!eventType.requiresConfirmation && !eventType.price) {
 | 
				
			||||||
    // Use EventManager to conditionally use all needed integrations.
 | 
					    // Use EventManager to conditionally use all needed integrations.
 | 
				
			||||||
    const createResults: CreateUpdateResult = await eventManager.create(evt);
 | 
					    const createResults = await eventManager.create(evt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    results = createResults.results;
 | 
					    results = createResults.results;
 | 
				
			||||||
    referencesToCreate = createResults.referencesToCreate;
 | 
					    referencesToCreate = createResults.referencesToCreate;
 | 
				
			||||||
| 
						 | 
					@ -461,46 +464,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  //for Daily.co video calls will grab the meeting token for the call
 | 
					 | 
				
			||||||
  const isDaily = evt.location === "integrations:daily";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let dailyEvent: DailyReturnType;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!rescheduleUid) {
 | 
					 | 
				
			||||||
    dailyEvent = results.filter((ref) => ref.type === "daily")[0]?.createdEvent as DailyReturnType;
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    dailyEvent = results.filter((ref) => ref.type === "daily_video")[0]?.updatedEvent as DailyReturnType;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let meetingToken;
 | 
					 | 
				
			||||||
  if (isDaily) {
 | 
					 | 
				
			||||||
    const response = await fetch("https://api.daily.co/v1/meeting-tokens", {
 | 
					 | 
				
			||||||
      method: "POST",
 | 
					 | 
				
			||||||
      body: JSON.stringify({ properties: { room_name: dailyEvent.name, is_owner: true } }),
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        Authorization: "Bearer " + process.env.DAILY_API_KEY,
 | 
					 | 
				
			||||||
        "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    meetingToken = await response.json();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  //for Daily.co video calls will update the dailyEventReference table
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (isDaily) {
 | 
					 | 
				
			||||||
    await prisma.dailyEventReference.create({
 | 
					 | 
				
			||||||
      data: {
 | 
					 | 
				
			||||||
        dailyurl: dailyEvent.url,
 | 
					 | 
				
			||||||
        dailytoken: meetingToken.token,
 | 
					 | 
				
			||||||
        booking: {
 | 
					 | 
				
			||||||
          connect: {
 | 
					 | 
				
			||||||
            uid: booking.uid,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (eventType.requiresConfirmation && !rescheduleUid) {
 | 
					  if (eventType.requiresConfirmation && !rescheduleUid) {
 | 
				
			||||||
    await new EventOrganizerRequestMail({ ...evt, uid }).sendEmail();
 | 
					    await new EventOrganizerRequestMail({ ...evt, uid }).sendEmail();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,19 +1,21 @@
 | 
				
			||||||
import { BookingStatus } from "@prisma/client";
 | 
					import { BookingStatus } from "@prisma/client";
 | 
				
			||||||
import async from "async";
 | 
					import async from "async";
 | 
				
			||||||
 | 
					import { NextApiRequest, NextApiResponse } from "next";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { refund } from "@ee/lib/stripe/server";
 | 
					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 { CalendarEvent, deleteEvent } from "@lib/calendarClient";
 | 
					import { CalendarEvent, deleteEvent } from "@lib/calendarClient";
 | 
				
			||||||
 | 
					import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
 | 
				
			||||||
import prisma from "@lib/prisma";
 | 
					import prisma from "@lib/prisma";
 | 
				
			||||||
import { deleteMeeting } from "@lib/videoClient";
 | 
					import { deleteMeeting } from "@lib/videoClient";
 | 
				
			||||||
import sendPayload from "@lib/webhooks/sendPayload";
 | 
					import sendPayload from "@lib/webhooks/sendPayload";
 | 
				
			||||||
import getSubscriberUrls from "@lib/webhooks/subscriberUrls";
 | 
					import getSubscriberUrls from "@lib/webhooks/subscriberUrls";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { dailyDeleteMeeting } from "../../lib/dailyVideoClient";
 | 
					import { getTranslation } from "@server/lib/i18n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default async function handler(req, res) {
 | 
					export default async function handler(req: NextApiRequest, res: NextApiResponse) {
 | 
				
			||||||
  // just bail if it not a DELETE
 | 
					  // just bail if it not a DELETE
 | 
				
			||||||
  if (req.method !== "DELETE" && req.method !== "POST") {
 | 
					  if (req.method !== "DELETE" && req.method !== "POST") {
 | 
				
			||||||
    return res.status(405).end();
 | 
					    return res.status(405).end();
 | 
				
			||||||
| 
						 | 
					@ -48,7 +50,6 @@ export default async function handler(req, res) {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      payment: true,
 | 
					      payment: true,
 | 
				
			||||||
      paid: true,
 | 
					      paid: true,
 | 
				
			||||||
      location: true,
 | 
					 | 
				
			||||||
      title: true,
 | 
					      title: true,
 | 
				
			||||||
      description: true,
 | 
					      description: true,
 | 
				
			||||||
      startTime: true,
 | 
					      startTime: true,
 | 
				
			||||||
| 
						 | 
					@ -62,32 +63,45 @@ export default async function handler(req, res) {
 | 
				
			||||||
    return res.status(404).end();
 | 
					    return res.status(404).end();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if ((!session || session.user?.id != bookingToDelete.user?.id) && bookingToDelete.startTime < new Date()) {
 | 
					  if ((!session || session.user?.id !== bookingToDelete.user?.id) && bookingToDelete.startTime < new Date()) {
 | 
				
			||||||
    return res.status(403).json({ message: "Cannot cancel past events" });
 | 
					    return res.status(403).json({ message: "Cannot cancel past events" });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!bookingToDelete.userId) {
 | 
				
			||||||
 | 
					    return res.status(404).json({ message: "User not found" });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const organizer = await prisma.user.findFirst({
 | 
					  const organizer = await prisma.user.findFirst({
 | 
				
			||||||
    where: {
 | 
					    where: {
 | 
				
			||||||
      id: bookingToDelete.userId as number,
 | 
					      id: bookingToDelete.userId,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    select: {
 | 
					    select: {
 | 
				
			||||||
      name: true,
 | 
					      name: true,
 | 
				
			||||||
      email: true,
 | 
					      email: true,
 | 
				
			||||||
      timeZone: true,
 | 
					      timeZone: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    rejectOnNotFound: true,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const t = await getTranslation(req.body.language ?? "en", "common");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const evt: CalendarEvent = {
 | 
					  const evt: CalendarEvent = {
 | 
				
			||||||
    type: bookingToDelete?.title,
 | 
					    type: bookingToDelete?.title,
 | 
				
			||||||
    title: bookingToDelete?.title,
 | 
					    title: bookingToDelete?.title,
 | 
				
			||||||
    description: bookingToDelete?.description || "",
 | 
					    description: bookingToDelete?.description || "",
 | 
				
			||||||
    startTime: bookingToDelete?.startTime.toString(),
 | 
					    startTime: bookingToDelete?.startTime.toString(),
 | 
				
			||||||
    endTime: bookingToDelete?.endTime.toString(),
 | 
					    endTime: bookingToDelete?.endTime.toString(),
 | 
				
			||||||
    organizer: organizer,
 | 
					    organizer: {
 | 
				
			||||||
 | 
					      email: organizer.email,
 | 
				
			||||||
 | 
					      name: organizer.name ?? "Nameless",
 | 
				
			||||||
 | 
					      timeZone: organizer.timeZone,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    attendees: bookingToDelete?.attendees.map((attendee) => {
 | 
					    attendees: bookingToDelete?.attendees.map((attendee) => {
 | 
				
			||||||
      const retObj = { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
 | 
					      const retObj = { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
 | 
				
			||||||
      return retObj;
 | 
					      return retObj;
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 | 
					    uid: bookingToDelete?.uid,
 | 
				
			||||||
 | 
					    language: t,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Hook up the webhook logic here
 | 
					  // Hook up the webhook logic here
 | 
				
			||||||
| 
						 | 
					@ -112,6 +126,10 @@ export default async function handler(req, res) {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (bookingToDelete.location === "integrations:daily") {
 | 
				
			||||||
 | 
					    bookingToDelete.user.credentials.push(FAKE_DAILY_CREDENTIAL);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential) => {
 | 
					  const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential) => {
 | 
				
			||||||
    const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0]?.uid;
 | 
					    const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0]?.uid;
 | 
				
			||||||
    if (bookingRefUid) {
 | 
					    if (bookingRefUid) {
 | 
				
			||||||
| 
						 | 
					@ -121,13 +139,6 @@ export default async function handler(req, res) {
 | 
				
			||||||
        return await deleteMeeting(credential, bookingRefUid);
 | 
					        return await deleteMeeting(credential, bookingRefUid);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    //deleting a Daily meeting
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isDaily = bookingToDelete.location === "integrations:daily";
 | 
					 | 
				
			||||||
    const bookingUID = bookingToDelete.references.filter((ref) => ref.type === "daily")[0]?.uid;
 | 
					 | 
				
			||||||
    if (isDaily) {
 | 
					 | 
				
			||||||
      return await dailyDeleteMeeting(credential, bookingUID);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (bookingToDelete && bookingToDelete.paid) {
 | 
					  if (bookingToDelete && bookingToDelete.paid) {
 | 
				
			||||||
| 
						 | 
					@ -144,6 +155,8 @@ export default async function handler(req, res) {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      attendees: bookingToDelete.attendees,
 | 
					      attendees: bookingToDelete.attendees,
 | 
				
			||||||
      location: bookingToDelete.location ?? "",
 | 
					      location: bookingToDelete.location ?? "",
 | 
				
			||||||
 | 
					      uid: bookingToDelete.uid ?? "",
 | 
				
			||||||
 | 
					      language: t,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    await refund(bookingToDelete, evt);
 | 
					    await refund(bookingToDelete, evt);
 | 
				
			||||||
    await prisma.booking.update({
 | 
					    await prisma.booking.update({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,8 @@ import { CalendarEvent } from "@lib/calendarClient";
 | 
				
			||||||
import EventOrganizerRequestReminderMail from "@lib/emails/EventOrganizerRequestReminderMail";
 | 
					import EventOrganizerRequestReminderMail from "@lib/emails/EventOrganizerRequestReminderMail";
 | 
				
			||||||
import prisma from "@lib/prisma";
 | 
					import prisma from "@lib/prisma";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getTranslation } from "@server/lib/i18n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
 | 
					export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
 | 
				
			||||||
  const apiKey = req.headers.authorization || req.query.apiKey;
 | 
					  const apiKey = req.headers.authorization || req.query.apiKey;
 | 
				
			||||||
  if (process.env.CRON_API_KEY !== apiKey) {
 | 
					  if (process.env.CRON_API_KEY !== apiKey) {
 | 
				
			||||||
| 
						 | 
					@ -31,6 +33,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
      select: {
 | 
					      select: {
 | 
				
			||||||
        title: true,
 | 
					        title: true,
 | 
				
			||||||
        description: true,
 | 
					        description: true,
 | 
				
			||||||
 | 
					        location: true,
 | 
				
			||||||
        startTime: true,
 | 
					        startTime: true,
 | 
				
			||||||
        endTime: true,
 | 
					        endTime: true,
 | 
				
			||||||
        attendees: true,
 | 
					        attendees: true,
 | 
				
			||||||
| 
						 | 
					@ -59,10 +62,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
        console.error(`Booking ${booking.id} is missing required properties for booking reminder`, { user });
 | 
					        console.error(`Booking ${booking.id} is missing required properties for booking reminder`, { user });
 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      const t = await getTranslation(req.body.language ?? "en", "common");
 | 
				
			||||||
      const evt: CalendarEvent = {
 | 
					      const evt: CalendarEvent = {
 | 
				
			||||||
        type: booking.title,
 | 
					        type: booking.title,
 | 
				
			||||||
        title: booking.title,
 | 
					        title: booking.title,
 | 
				
			||||||
        description: booking.description || undefined,
 | 
					        description: booking.description || undefined,
 | 
				
			||||||
 | 
					        location: booking.location ?? "",
 | 
				
			||||||
        startTime: booking.startTime.toISOString(),
 | 
					        startTime: booking.startTime.toISOString(),
 | 
				
			||||||
        endTime: booking.endTime.toISOString(),
 | 
					        endTime: booking.endTime.toISOString(),
 | 
				
			||||||
        organizer: {
 | 
					        organizer: {
 | 
				
			||||||
| 
						 | 
					@ -71,9 +76,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
          timeZone: user.timeZone,
 | 
					          timeZone: user.timeZone,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        attendees: booking.attendees,
 | 
					        attendees: booking.attendees,
 | 
				
			||||||
 | 
					        uid: booking.uid,
 | 
				
			||||||
 | 
					        language: t,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await new EventOrganizerRequestReminderMail(evt, booking.uid).sendEmail();
 | 
					      await new EventOrganizerRequestReminderMail(evt).sendEmail();
 | 
				
			||||||
      await prisma.reminderMail.create({
 | 
					      await prisma.reminderMail.create({
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
          referenceId: booking.id,
 | 
					          referenceId: booking.id,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1113,6 +1113,11 @@
 | 
				
			||||||
  resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
 | 
					  resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
 | 
				
			||||||
  integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
 | 
					  integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@microsoft/microsoft-graph-types-beta@0.15.0-preview":
 | 
				
			||||||
 | 
					  version "0.15.0-preview"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@microsoft/microsoft-graph-types-beta/-/microsoft-graph-types-beta-0.15.0-preview.tgz#fed0a99be4e1151d566cf063f024913fb48640cd"
 | 
				
			||||||
 | 
					  integrity sha512-M0zC4t3pmkDz7Qsjx/iZcS+zRuckzsbHESvT9qjLFv64RUgkRmDdmhcvPMiUqUzw/h3YxfYAq9MU+XWjROk/dg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@napi-rs/triples@^1.0.3":
 | 
					"@napi-rs/triples@^1.0.3":
 | 
				
			||||||
  version "1.0.3"
 | 
					  version "1.0.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.0.3.tgz#76d6d0c3f4d16013c61e45dfca5ff1e6c31ae53c"
 | 
					  resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.0.3.tgz#76d6d0c3f4d16013c61e45dfca5ff1e6c31ae53c"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue