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:
Pavel Shapel 2022-08-11 03:53:05 +03:00 committed by GitHub
parent 14f0c30584
commit 6d79f80928
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 426 additions and 370 deletions

View File

@ -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,
},
};
}

View File

@ -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">
&#8203;
</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">
&#8203;
</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,
},
};
}

View File

@ -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">
&#8203;
</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">
&#8203;
</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,
},
};
}

View File

@ -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),
};
};

View File

@ -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);
};

View File

@ -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;

View File

@ -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

View File

@ -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 ?? "";
};

View File

@ -0,0 +1,4 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};

View File

@ -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"
}
}

View File

@ -0,0 +1 @@
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000

View File

@ -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);
});
});

View File

@ -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,
};
};

View File

@ -0,0 +1,4 @@
UPDATE "BookingReference"
SET "meetingUrl" = "dailyurl", "meetingPassword" = "dailytoken"
FROM "DailyEventReference"
WHERE "DailyEventReference"."bookingId" = "BookingReference"."bookingId" AND "BookingReference"."type" = 'daily_video'

View File

@ -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";

View File

@ -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)

View File

@ -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;