refactor: use BookingReference instead of DailyEventReference (#3667)
* refactor: use BookingReference instead of DailyEventReference * refactor: migrate DailyEventReference records to BookingReference * refactor: drop DailyEventReference table * fix linting * Daily Video API adapter fixes Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
parent
14f0c30584
commit
6d79f80928
|
@ -3,10 +3,9 @@ import { NextPageContext } from "next";
|
|||
import { getSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { WEBSITE_URL, SEO_IMG_OGIMG_VIDEO } from "@calcom/lib/constants";
|
||||
import { SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
|
@ -15,96 +14,38 @@ export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
|
|||
|
||||
export default function JoinCall(props: JoinCallPageProps) {
|
||||
const { t } = useLocale();
|
||||
const session = props.session;
|
||||
const router = useRouter();
|
||||
|
||||
//if no booking redirectis to the 404 page
|
||||
const emptyBooking = props.booking === null;
|
||||
|
||||
//daily.co calls have a 60 minute exit buffer when a user enters a call when it's not available it will trigger the modals
|
||||
const now = new Date();
|
||||
const exitDate = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
|
||||
//find out if the meeting is in the past
|
||||
const isPast = new Date(props.booking?.endTime || "") <= exitDate;
|
||||
const meetingUnavailable = isPast == true;
|
||||
|
||||
useEffect(() => {
|
||||
if (emptyBooking) {
|
||||
router.push("/video/no-meeting-found");
|
||||
}
|
||||
|
||||
if (isPast) {
|
||||
router.push(`/video/meeting-ended/${props.booking?.uid}`);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!meetingUnavailable && !emptyBooking && session?.userid !== props.booking.user?.id) {
|
||||
const callFrame = DailyIframe.createFrame({
|
||||
theme: {
|
||||
colors: {
|
||||
accent: "#FFF",
|
||||
accentText: "#111111",
|
||||
background: "#111111",
|
||||
backgroundAccent: "#111111",
|
||||
baseText: "#FFF",
|
||||
border: "#292929",
|
||||
mainAreaBg: "#111111",
|
||||
mainAreaBgAccent: "#111111",
|
||||
mainAreaText: "#FFF",
|
||||
supportiveText: "#FFF",
|
||||
},
|
||||
const callFrame = DailyIframe.createFrame({
|
||||
theme: {
|
||||
colors: {
|
||||
accent: "#FFF",
|
||||
accentText: "#111111",
|
||||
background: "#111111",
|
||||
backgroundAccent: "#111111",
|
||||
baseText: "#FFF",
|
||||
border: "#292929",
|
||||
mainAreaBg: "#111111",
|
||||
mainAreaBgAccent: "#111111",
|
||||
mainAreaText: "#FFF",
|
||||
supportiveText: "#FFF",
|
||||
},
|
||||
showLeaveButton: true,
|
||||
iframeStyle: {
|
||||
position: "fixed",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
});
|
||||
callFrame.join({
|
||||
url: props.booking.dailyRef?.dailyurl,
|
||||
showLeaveButton: true,
|
||||
});
|
||||
}
|
||||
if (!meetingUnavailable && !emptyBooking && session?.userid === props.booking.user?.id) {
|
||||
const callFrame = DailyIframe.createFrame({
|
||||
theme: {
|
||||
colors: {
|
||||
accent: "#FFF",
|
||||
accentText: "#111111",
|
||||
background: "#111111",
|
||||
backgroundAccent: "#111111",
|
||||
baseText: "#FFF",
|
||||
border: "#292929",
|
||||
mainAreaBg: "#111111",
|
||||
mainAreaBgAccent: "#111111",
|
||||
mainAreaText: "#FFF",
|
||||
supportiveText: "#FFF",
|
||||
},
|
||||
},
|
||||
showLeaveButton: true,
|
||||
iframeStyle: {
|
||||
position: "fixed",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
});
|
||||
callFrame.join({
|
||||
url: props.booking.dailyRef?.dailyurl,
|
||||
showLeaveButton: true,
|
||||
token: props.booking.dailyRef?.dailytoken,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
emptyBooking,
|
||||
meetingUnavailable,
|
||||
props.booking?.dailyRef?.dailytoken,
|
||||
props.booking?.dailyRef?.dailyurl,
|
||||
props.booking?.user?.id,
|
||||
session?.userid,
|
||||
]);
|
||||
},
|
||||
showLeaveButton: true,
|
||||
iframeStyle: {
|
||||
position: "fixed",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
});
|
||||
callFrame.join({
|
||||
url: props.booking.references[0].meetingUrl ?? "",
|
||||
showLeaveButton: true,
|
||||
...(props.booking.references[0].meetingPassword
|
||||
? { token: props.booking.references[0].meetingPassword }
|
||||
: null),
|
||||
});
|
||||
}, [props.booking?.references]);
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
@ -123,23 +64,20 @@ export default function JoinCall(props: JoinCallPageProps) {
|
|||
<meta property="twitter:description" content={t("quick_video_meeting")} />
|
||||
</Head>
|
||||
<div style={{ zIndex: 2, position: "relative" }}>
|
||||
<>
|
||||
<Link href="/" passHref>
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="h-5·w-auto fixed z-10 hidden sm:inline-block"
|
||||
src={`${WEBSITE_URL}/logo-white.svg`}
|
||||
alt="Cal.com Logo"
|
||||
style={{
|
||||
top: 46,
|
||||
left: 24,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Link>
|
||||
{JoinCall}
|
||||
</>
|
||||
<Link href="/" passHref>
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="h-5·w-auto fixed z-10 hidden sm:inline-block"
|
||||
src={`${WEBSITE_URL}/calendso-logo-white-word.svg`}
|
||||
alt="Cal.com Logo"
|
||||
style={{
|
||||
top: 46,
|
||||
left: 24,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -159,25 +97,41 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
credentials: true,
|
||||
},
|
||||
},
|
||||
dailyRef: {
|
||||
select: {
|
||||
dailyurl: true,
|
||||
dailytoken: true,
|
||||
},
|
||||
},
|
||||
references: {
|
||||
select: {
|
||||
uid: true,
|
||||
type: true,
|
||||
meetingUrl: true,
|
||||
meetingPassword: true,
|
||||
},
|
||||
where: {
|
||||
type: "daily_video",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
// TODO: Booking is already cancelled
|
||||
if (!booking || booking.references.length === 0 || !booking.references[0].meetingUrl) {
|
||||
return {
|
||||
props: { booking: null },
|
||||
redirect: {
|
||||
destination: "/video/no-meeting-found",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
//daily.co calls have a 60 minute exit buffer when a user enters a call when it's not available it will trigger the modals
|
||||
const now = new Date();
|
||||
const exitDate = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
|
||||
//find out if the meeting is in the past
|
||||
const isPast = booking?.endTime <= exitDate;
|
||||
if (isPast) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/video/meeting-ended/${booking?.uid}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -187,10 +141,16 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
});
|
||||
const session = await getSession();
|
||||
|
||||
// set meetingPassword to null for guests
|
||||
if (session?.userid !== bookingObj.user?.id) {
|
||||
bookingObj.references.forEach((bookRef) => {
|
||||
bookRef.meetingPassword = null;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking: bookingObj,
|
||||
session: session,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import { NextPageContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
@ -16,67 +13,55 @@ import { HeadSeo } from "@components/seo/head-seo";
|
|||
|
||||
export default function MeetingUnavailable(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
// if no booking redirectis to the 404 page
|
||||
const emptyBooking = props.booking === null;
|
||||
useEffect(() => {
|
||||
if (emptyBooking) {
|
||||
router.push("/video/no-meeting-found");
|
||||
}
|
||||
});
|
||||
if (!emptyBooking) {
|
||||
return (
|
||||
<div>
|
||||
<HeadSeo title="Meeting Unavaialble" description="Meeting Unavailable" />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
<div
|
||||
className="inline-block transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6 sm:align-middle"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
|
||||
This meeting is in the past.
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-b py-4">
|
||||
<h2 className="font-cal mb-2 text-center text-lg font-medium text-gray-600">
|
||||
{props.booking.title}
|
||||
</h2>
|
||||
<p className="text-center text-gray-500">
|
||||
<Icon.FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{dayjs(props.booking.startTime).format(
|
||||
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeadSeo title="Meeting Unavailable" description="Meeting Unavailable" />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
<div
|
||||
className="inline-block transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6 sm:align-middle"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-5 text-center sm:mt-6">
|
||||
<div className="mt-5">
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={Icon.FiArrowRight}>
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
|
||||
This meeting is in the past.
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-b py-4">
|
||||
<h2 className="font-cal mb-2 text-center text-lg font-medium text-gray-600">
|
||||
{props.booking.title}
|
||||
</h2>
|
||||
<p className="text-center text-gray-500">
|
||||
<Icon.FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 text-center sm:mt-6">
|
||||
<div className="mt-5">
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={Icon.FiArrowRight}>
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
|
@ -92,25 +77,22 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
credentials: true,
|
||||
},
|
||||
},
|
||||
dailyRef: {
|
||||
select: {
|
||||
dailyurl: true,
|
||||
dailytoken: true,
|
||||
},
|
||||
},
|
||||
references: {
|
||||
select: {
|
||||
uid: true,
|
||||
type: true,
|
||||
meetingUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
// TODO: Booking is already cancelled
|
||||
return {
|
||||
props: { booking: null },
|
||||
redirect: {
|
||||
destination: "/video/no-meeting-found",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -118,12 +100,10 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
startTime: booking.startTime.toString(),
|
||||
endTime: booking.endTime.toString(),
|
||||
});
|
||||
const session = await getSession();
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking: bookingObj,
|
||||
session: session,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import { NextPageContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
@ -15,73 +12,59 @@ import { HeadSeo } from "@components/seo/head-seo";
|
|||
|
||||
export default function MeetingNotStarted(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
//if no booking redirectis to the 404 page
|
||||
const emptyBooking = props.booking === null;
|
||||
useEffect(() => {
|
||||
if (emptyBooking) {
|
||||
router.push("/video/no-meeting-found");
|
||||
}
|
||||
});
|
||||
if (!emptyBooking) {
|
||||
return (
|
||||
<div>
|
||||
<HeadSeo title="Meeting Unavaialble" description="Meeting Unavailable" />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
<div
|
||||
className="inline-block transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6 sm:align-middle"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
|
||||
This meeting has not started yet
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-b py-4">
|
||||
<h2 className="font-cal mb-2 text-center text-lg font-medium text-gray-600">
|
||||
{props.booking.title}
|
||||
</h2>
|
||||
<p className="text-center text-gray-500">
|
||||
<Icon.FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{dayjs(props.booking.startTime).format(
|
||||
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<p className="text-sm text-gray-500">
|
||||
This meeting will be accessible 60 minutes in advance.
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<div>
|
||||
<HeadSeo title="Meeting Unavailable" description="Meeting Unavailable" />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
<div
|
||||
className="inline-block transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6 sm:align-middle"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-5 text-center sm:mt-6">
|
||||
<div className="mt-5">
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={Icon.FiArrowRight}>
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
|
||||
This meeting has not started yet
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-b py-4">
|
||||
<h2 className="font-cal mb-2 text-center text-lg font-medium text-gray-600">
|
||||
{props.booking.title}
|
||||
</h2>
|
||||
<p className="text-center text-gray-500">
|
||||
<Icon.FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<p className="text-sm text-gray-500">
|
||||
This meeting will be accessible 60 minutes in advance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 text-center sm:mt-6">
|
||||
<div className="mt-5">
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={Icon.FiArrowRight}>
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
|
@ -89,33 +72,15 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
where: {
|
||||
uid: context.query.uid as string,
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
uid: true,
|
||||
user: {
|
||||
select: {
|
||||
credentials: true,
|
||||
},
|
||||
},
|
||||
dailyRef: {
|
||||
select: {
|
||||
dailyurl: true,
|
||||
dailytoken: true,
|
||||
},
|
||||
},
|
||||
references: {
|
||||
select: {
|
||||
uid: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: bookingMinimalSelect,
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
// TODO: Booking is already cancelled
|
||||
return {
|
||||
props: { booking: null },
|
||||
redirect: {
|
||||
destination: "/video/no-meeting-found",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -123,12 +88,10 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
startTime: booking.startTime.toString(),
|
||||
endTime: booking.endTime.toString(),
|
||||
});
|
||||
const session = await getSession();
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking: bookingObj,
|
||||
session: session,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,34 +1,35 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { handleErrorsJson } from "@calcom/lib/errors";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import { getDailyAppKeys } from "./getDailyAppKeys";
|
||||
|
||||
/** @link https://docs.daily.co/reference/rest-api/rooms/create-room */
|
||||
export interface DailyReturnType {
|
||||
const dailyReturnTypeSchema = z.object({
|
||||
/** Long UID string ie: 987b5eb5-d116-4a4e-8e2c-14fcb5710966 */
|
||||
id: string;
|
||||
id: z.string(),
|
||||
/** Not a real name, just a random generated string ie: "ePR84NQ1bPigp79dDezz" */
|
||||
name: string;
|
||||
api_created: boolean;
|
||||
privacy: "private" | "public";
|
||||
name: z.string(),
|
||||
api_created: z.boolean(),
|
||||
privacy: z.union([z.literal("private"), z.literal("public")]),
|
||||
/** https://api-demo.daily.co/ePR84NQ1bPigp79dDezz */
|
||||
url: string;
|
||||
created_at: string;
|
||||
config: {
|
||||
url: z.string(),
|
||||
created_at: z.string(),
|
||||
config: z.object({
|
||||
/** Timestamps expressed in seconds, not in milliseconds */
|
||||
nbf: number;
|
||||
nbf: z.number().optional(),
|
||||
/** Timestamps expressed in seconds, not in milliseconds */
|
||||
exp: number;
|
||||
enable_chat: boolean;
|
||||
enable_knocking: boolean;
|
||||
enable_prejoin_ui: boolean;
|
||||
enable_new_call_ui: boolean;
|
||||
};
|
||||
}
|
||||
exp: z.number(),
|
||||
enable_chat: z.boolean(),
|
||||
enable_knocking: z.boolean(),
|
||||
enable_prejoin_ui: z.boolean(),
|
||||
enable_new_call_ui: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export interface DailyEventResult {
|
||||
id: string;
|
||||
|
@ -47,9 +48,9 @@ export interface DailyVideoCallData {
|
|||
url: string;
|
||||
}
|
||||
|
||||
type DailyKey = {
|
||||
apikey: string;
|
||||
};
|
||||
const meetingTokenSchema = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
/** @deprecated use metadata on index file */
|
||||
export const FAKE_DAILY_CREDENTIAL: Credential = {
|
||||
|
@ -60,47 +61,43 @@ export const FAKE_DAILY_CREDENTIAL: Credential = {
|
|||
appId: "daily-video",
|
||||
};
|
||||
|
||||
const DailyVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
||||
const dailyApiToken = (credential.key as DailyKey).apikey;
|
||||
const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
|
||||
const { api_key } = await getDailyAppKeys();
|
||||
return fetch(`https://api.daily.co/v1${endpoint}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: "Bearer " + api_key,
|
||||
"Content-Type": "application/json",
|
||||
...init?.headers,
|
||||
},
|
||||
...init,
|
||||
}).then(handleErrorsJson);
|
||||
};
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
function postToDailyAPI(endpoint: string, body: Record<string, any>) {
|
||||
return fetcher(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
const DailyVideoApiAdapter = (): VideoApiAdapter => {
|
||||
async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent): Promise<VideoCallData> {
|
||||
if (!event.uid) {
|
||||
throw new Error("We need need the booking uid to create the Daily reference in DB");
|
||||
}
|
||||
const response = await postToDailyAPI(endpoint, translateEvent(event));
|
||||
const dailyEvent = (await handleErrorsJson(response)) as DailyReturnType;
|
||||
const res = await postToDailyAPI("/meeting-tokens", {
|
||||
const dailyEvent = await postToDailyAPI(endpoint, translateEvent(event)).then(
|
||||
dailyReturnTypeSchema.parse
|
||||
);
|
||||
const meetingToken = await postToDailyAPI("/meeting-tokens", {
|
||||
properties: { room_name: dailyEvent.name, is_owner: true },
|
||||
});
|
||||
const meetingToken = (await handleErrorsJson(res)) as { token: string };
|
||||
await prisma.dailyEventReference.create({
|
||||
data: {
|
||||
dailyurl: dailyEvent.url,
|
||||
dailytoken: meetingToken.token,
|
||||
booking: {
|
||||
connect: {
|
||||
uid: event.uid,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}).then(meetingTokenSchema.parse);
|
||||
|
||||
return Promise.resolve({
|
||||
type: "daily_video",
|
||||
id: dailyEvent.name,
|
||||
password: "",
|
||||
url: WEBAPP_URL + "/video/" + event.uid,
|
||||
password: meetingToken.token,
|
||||
url: dailyEvent.url,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -145,17 +142,11 @@ const DailyVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
|||
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> =>
|
||||
createOrUpdateMeeting("/rooms", event),
|
||||
deleteMeeting: async (uid: string): Promise<void> => {
|
||||
await fetch("https://api.daily.co/v1/rooms/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + dailyApiToken,
|
||||
},
|
||||
}).then(handleErrorsJson);
|
||||
|
||||
await fetcher(`/rooms/${uid}`, { method: "DELETE" });
|
||||
return Promise.resolve();
|
||||
},
|
||||
updateMeeting: (bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> =>
|
||||
createOrUpdateMeeting("/rooms/" + bookingRef.uid, event),
|
||||
createOrUpdateMeeting(`/rooms/${bookingRef.uid}`, event),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
const dailyAppKeysSchema = z.object({
|
||||
api_key: z.string(),
|
||||
});
|
||||
|
||||
export const getDailyAppKeys = async () => {
|
||||
const appKeys = await getAppKeysFromSlug("daily-video");
|
||||
return dailyAppKeysSchema.parse(appKeys);
|
||||
};
|
|
@ -1,6 +1,12 @@
|
|||
import { DestinationCalendar } from "@prisma/client";
|
||||
|
||||
import type { AdditionalInformation, CalendarEvent, ConferenceData, Person } from "@calcom/types/Calendar";
|
||||
import type {
|
||||
AdditionalInformation,
|
||||
CalendarEvent,
|
||||
ConferenceData,
|
||||
Person,
|
||||
VideoCallData,
|
||||
} from "@calcom/types/Calendar";
|
||||
|
||||
class CalendarEventClass implements CalendarEvent {
|
||||
type!: string;
|
||||
|
@ -15,7 +21,7 @@ class CalendarEventClass implements CalendarEvent {
|
|||
conferenceData?: ConferenceData;
|
||||
additionalInformation?: AdditionalInformation;
|
||||
uid?: string | null;
|
||||
videoCallData?: any;
|
||||
videoCallData?: VideoCallData;
|
||||
paymentInfo?: any;
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
cancellationReason?: string | null;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { getAppName } from "@calcom/app-store/utils";
|
||||
import { getVideoCallPassword, getVideoCallUrl, getProviderName } from "@calcom/lib/CalEventParser";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
@ -8,22 +9,13 @@ import { LinkIcon } from "./LinkIcon";
|
|||
|
||||
export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
|
||||
const { t } = props;
|
||||
let providerName = props.calEvent.location && getAppName(props.calEvent.location);
|
||||
|
||||
if (props.calEvent.location && props.calEvent.location.includes("integrations:")) {
|
||||
const location = props.calEvent.location.split(":")[1];
|
||||
providerName = location[0].toUpperCase() + location.slice(1);
|
||||
}
|
||||
|
||||
// If location its a url, probably we should be validating it with a custom library
|
||||
if (props.calEvent.location && /^https?:\/\//.test(props.calEvent.location)) {
|
||||
providerName = props.calEvent.location;
|
||||
}
|
||||
const providerName =
|
||||
(props.calEvent.location && getAppName(props.calEvent.location)) || getProviderName(props.calEvent);
|
||||
|
||||
if (props.calEvent.videoCallData) {
|
||||
const meetingId = props.calEvent.videoCallData.id;
|
||||
const meetingPassword = props.calEvent.videoCallData.password;
|
||||
const meetingUrl = props.calEvent.videoCallData.url;
|
||||
const meetingPassword = getVideoCallPassword(props.calEvent);
|
||||
const meetingUrl = getVideoCallUrl(props.calEvent);
|
||||
|
||||
return (
|
||||
<Info
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Person } from "ics";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
|
@ -83,22 +82,25 @@ export const getDescription = (calEvent: CalendarEvent) => {
|
|||
`;
|
||||
};
|
||||
export const getLocation = (calEvent: CalendarEvent) => {
|
||||
let providerName = "";
|
||||
const meetingUrl = getVideoCallUrl(calEvent);
|
||||
if (meetingUrl) {
|
||||
return meetingUrl;
|
||||
}
|
||||
const providerName = getProviderName(calEvent);
|
||||
return providerName || calEvent.location || "";
|
||||
};
|
||||
|
||||
export const getProviderName = (calEvent: CalendarEvent): string => {
|
||||
// TODO: use getAppName from @calcom/app-store
|
||||
if (calEvent.location && calEvent.location.includes("integrations:")) {
|
||||
const location = calEvent.location.split(":")[1];
|
||||
providerName = location[0].toUpperCase() + location.slice(1);
|
||||
return location[0].toUpperCase() + location.slice(1);
|
||||
}
|
||||
|
||||
if (calEvent.videoCallData) {
|
||||
return calEvent.videoCallData.url;
|
||||
// If location its a url, probably we should be validating it with a custom library
|
||||
if (calEvent.location && /^https?:\/\//.test(calEvent.location)) {
|
||||
return calEvent.location;
|
||||
}
|
||||
|
||||
if (calEvent.additionalInformation?.hangoutLink) {
|
||||
return calEvent.additionalInformation.hangoutLink;
|
||||
}
|
||||
|
||||
return providerName || calEvent.location || "";
|
||||
return "";
|
||||
};
|
||||
|
||||
export const getManageLink = (calEvent: CalendarEvent) => {
|
||||
|
@ -150,3 +152,28 @@ ${calEvent.organizer.language.translate("cancellation_reason")}:
|
|||
${calEvent.cancellationReason}
|
||||
`;
|
||||
};
|
||||
|
||||
export const isDailyVideoCall = (calEvent: CalendarEvent): boolean => {
|
||||
return calEvent?.videoCallData?.type === "daily_video";
|
||||
};
|
||||
|
||||
export const getPublicVideoCallUrl = (calEvent: CalendarEvent): string => {
|
||||
return WEBAPP_URL + "/video/" + getUid(calEvent);
|
||||
};
|
||||
|
||||
export const getVideoCallUrl = (calEvent: CalendarEvent): string => {
|
||||
if (calEvent.videoCallData) {
|
||||
if (isDailyVideoCall(calEvent)) {
|
||||
return getPublicVideoCallUrl(calEvent);
|
||||
}
|
||||
return calEvent.videoCallData.url;
|
||||
}
|
||||
if (calEvent.additionalInformation?.hangoutLink) {
|
||||
return calEvent.additionalInformation.hangoutLink;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const getVideoCallPassword = (calEvent: CalendarEvent): string => {
|
||||
return isDailyVideoCall(calEvent) ? "" : calEvent?.videoCallData?.password ?? "";
|
||||
};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
};
|
|
@ -5,13 +5,14 @@
|
|||
"types": "./index.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "dotenv -e ./test/.env.test -- jest",
|
||||
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/config": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@prisma/client": "^4.1.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"ical.js": "^1.4.0",
|
||||
|
@ -26,6 +27,9 @@
|
|||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/types": "*",
|
||||
"@faker-js/faker": "^7.3.0",
|
||||
"jest": "^26.0.0",
|
||||
"ts-jest": "^26.0.0",
|
||||
"typescript": "^4.6.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
|
@ -0,0 +1,63 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
|
||||
import { getPublicVideoCallUrl, getLocation, getVideoCallPassword, getVideoCallUrl } from "../CalEventParser";
|
||||
import { buildCalendarEvent, buildVideoCallData } from "./builder";
|
||||
|
||||
describe("getLocation", () => {
|
||||
it("should return a meetingUrl for video call meetings", () => {
|
||||
const calEvent = buildCalendarEvent({
|
||||
videoCallData: buildVideoCallData({
|
||||
type: "daily_video",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getLocation(calEvent)).toEqual(getVideoCallUrl(calEvent));
|
||||
});
|
||||
it("should return an integration provider name from event", () => {
|
||||
const provideName = "Cal.com";
|
||||
const calEvent = buildCalendarEvent({
|
||||
videoCallData: undefined,
|
||||
location: `integrations:${provideName}`,
|
||||
});
|
||||
|
||||
expect(getLocation(calEvent)).toEqual(provideName);
|
||||
});
|
||||
it("should return a real-world location from event", () => {
|
||||
const calEvent = buildCalendarEvent({
|
||||
videoCallData: undefined,
|
||||
location: faker.address.streetAddress(true),
|
||||
});
|
||||
|
||||
expect(getLocation(calEvent)).toEqual(calEvent.location);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getVideoCallUrl", () => {
|
||||
it("should return an app public url instead of meeting url for daily call meetings", () => {
|
||||
const calEvent = buildCalendarEvent({
|
||||
videoCallData: buildVideoCallData({
|
||||
type: "daily_video",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getVideoCallUrl(calEvent)).toEqual(getPublicVideoCallUrl(calEvent));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getVideoCallPassword", () => {
|
||||
it("should return an empty password for daily call meetings", () => {
|
||||
const calEvent = buildCalendarEvent({
|
||||
videoCallData: buildVideoCallData({
|
||||
type: "daily_video",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getVideoCallPassword(calEvent)).toEqual("");
|
||||
});
|
||||
it("should return original password for other video call meetings", () => {
|
||||
const calEvent = buildCalendarEvent();
|
||||
|
||||
expect(calEvent?.videoCallData?.type).not.toBe("daily_video");
|
||||
expect(getVideoCallPassword(calEvent)).toEqual(calEvent?.videoCallData.password);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
|
||||
import { CalendarEvent, Person, VideoCallData } from "@calcom/types/Calendar";
|
||||
|
||||
export const buildVideoCallData = (callData?: Partial<VideoCallData>): VideoCallData => {
|
||||
return {
|
||||
type: faker.helpers.arrayElement(["zoom_video", "stream_video"]),
|
||||
id: faker.datatype.uuid(),
|
||||
password: faker.internet.password(),
|
||||
url: faker.internet.url(),
|
||||
...callData,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildPerson = (person?: Partial<Person>): Person => {
|
||||
return {
|
||||
name: faker.name.firstName(),
|
||||
email: faker.internet.email(),
|
||||
timeZone: faker.address.timeZone(),
|
||||
username: faker.internet.userName(),
|
||||
id: faker.datatype.uuid(),
|
||||
language: {
|
||||
locale: faker.random.locale(),
|
||||
translate: (key: string) => key,
|
||||
},
|
||||
...person,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildCalendarEvent = (event?: Partial<CalendarEvent>): CalendarEvent => {
|
||||
return {
|
||||
uid: faker.datatype.uuid(),
|
||||
type: faker.helpers.arrayElement(["event", "meeting"]),
|
||||
title: faker.lorem.sentence(),
|
||||
startTime: faker.date.future().toISOString(),
|
||||
endTime: faker.date.future().toISOString(),
|
||||
location: faker.address.city(),
|
||||
description: faker.lorem.paragraph(),
|
||||
attendees: [],
|
||||
customInputs: {},
|
||||
additionalNotes: faker.lorem.paragraph(),
|
||||
organizer: buildPerson(),
|
||||
videoCallData: buildVideoCallData(),
|
||||
...event,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
UPDATE "BookingReference"
|
||||
SET "meetingUrl" = "dailyurl", "meetingPassword" = "dailytoken"
|
||||
FROM "DailyEventReference"
|
||||
WHERE "DailyEventReference"."bookingId" = "BookingReference"."bookingId" AND "BookingReference"."type" = 'daily_video'
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `DailyEventReference` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "DailyEventReference" DROP CONSTRAINT "DailyEventReference_bookingId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "DailyEventReference";
|
|
@ -260,14 +260,6 @@ enum BookingStatus {
|
|||
PENDING @map("pending")
|
||||
}
|
||||
|
||||
model DailyEventReference {
|
||||
id Int @id @default(autoincrement())
|
||||
dailyurl String @default("dailycallurl")
|
||||
dailytoken String @default("dailytoken")
|
||||
booking Booking? @relation(fields: [bookingId], references: [id])
|
||||
bookingId Int? @unique
|
||||
}
|
||||
|
||||
model Booking {
|
||||
id Int @id @default(autoincrement())
|
||||
uid String @unique
|
||||
|
@ -283,7 +275,6 @@ model Booking {
|
|||
endTime DateTime
|
||||
attendees Attendee[]
|
||||
location String?
|
||||
dailyRef DailyEventReference?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime?
|
||||
status BookingStatus @default(ACCEPTED)
|
||||
|
|
|
@ -6,9 +6,10 @@ import type { TFunction } from "next-i18next";
|
|||
|
||||
import type { Frequency } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { Event } from "./Event";
|
||||
import type { Ensure } from "./utils";
|
||||
|
||||
export type { VideoCallData } from "./VideoApiAdapter";
|
||||
|
||||
type PaymentInfo = {
|
||||
link?: string | null;
|
||||
reason?: string | null;
|
||||
|
|
Loading…
Reference in New Issue
Block a user