-
-
-
-
-
- This meeting has not started yet
-
-
-
-
- {props.booking.title}
-
-
-
- {dayjs(props.booking.startTime).format(
- detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
- )}
-
-
-
-
- This meeting will be accessible 60 minutes in advance.
-
-
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+ This meeting has not started yet
+
+
+
+
+ {props.booking.title}
+
+
+
+ {dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
+
+
+
+
+ This meeting will be accessible 60 minutes in advance.
+
+
+
+
-
-
- );
- }
- return null;
+
+
+
+ );
}
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,
},
};
}
diff --git a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts
index 7feb68b348..4c6f503a1d 100644
--- a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts
+++ b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts
@@ -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
) {
- 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) {
+ return fetcher(endpoint, {
+ method: "POST",
+ body: JSON.stringify(body),
+ });
+}
+const DailyVideoApiAdapter = (): VideoApiAdapter => {
async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent): Promise {
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 =>
createOrUpdateMeeting("/rooms", event),
deleteMeeting: async (uid: string): Promise => {
- 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 =>
- createOrUpdateMeeting("/rooms/" + bookingRef.uid, event),
+ createOrUpdateMeeting(`/rooms/${bookingRef.uid}`, event),
};
};
diff --git a/packages/app-store/dailyvideo/lib/getDailyAppKeys.ts b/packages/app-store/dailyvideo/lib/getDailyAppKeys.ts
new file mode 100644
index 0000000000..adc04f7a36
--- /dev/null
+++ b/packages/app-store/dailyvideo/lib/getDailyAppKeys.ts
@@ -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);
+};
diff --git a/packages/core/builders/CalendarEvent/class.ts b/packages/core/builders/CalendarEvent/class.ts
index d74d9a3739..168fb08043 100644
--- a/packages/core/builders/CalendarEvent/class.ts
+++ b/packages/core/builders/CalendarEvent/class.ts
@@ -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;
diff --git a/packages/emails/src/components/LocationInfo.tsx b/packages/emails/src/components/LocationInfo.tsx
index 63f6651b64..2c38e10751 100644
--- a/packages/emails/src/components/LocationInfo.tsx
+++ b/packages/emails/src/components/LocationInfo.tsx
@@ -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 (
{
`;
};
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 ?? "";
+};
diff --git a/packages/lib/jest.config.js b/packages/lib/jest.config.js
new file mode 100644
index 0000000000..a93551fca2
--- /dev/null
+++ b/packages/lib/jest.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ preset: "ts-jest",
+ testEnvironment: "node",
+};
diff --git a/packages/lib/package.json b/packages/lib/package.json
index 2963e09239..74431fc664 100644
--- a/packages/lib/package.json
+++ b/packages/lib/package.json
@@ -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"
}
}
diff --git a/packages/lib/test/.env.test b/packages/lib/test/.env.test
new file mode 100644
index 0000000000..a0649dd7d7
--- /dev/null
+++ b/packages/lib/test/.env.test
@@ -0,0 +1 @@
+NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
diff --git a/packages/lib/test/CalEventParser.test.ts b/packages/lib/test/CalEventParser.test.ts
new file mode 100644
index 0000000000..28fa758301
--- /dev/null
+++ b/packages/lib/test/CalEventParser.test.ts
@@ -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);
+ });
+});
diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts
new file mode 100644
index 0000000000..ce0f94d149
--- /dev/null
+++ b/packages/lib/test/builder.ts
@@ -0,0 +1,46 @@
+import { faker } from "@faker-js/faker";
+
+import { CalendarEvent, Person, VideoCallData } from "@calcom/types/Calendar";
+
+export const buildVideoCallData = (callData?: Partial): 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 => {
+ 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 => {
+ 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,
+ };
+};
diff --git a/packages/prisma/migrations/20220803090845_migrate_daily_event_reference_to_booking_reference/migration.sql b/packages/prisma/migrations/20220803090845_migrate_daily_event_reference_to_booking_reference/migration.sql
new file mode 100644
index 0000000000..c384e45e53
--- /dev/null
+++ b/packages/prisma/migrations/20220803090845_migrate_daily_event_reference_to_booking_reference/migration.sql
@@ -0,0 +1,4 @@
+UPDATE "BookingReference"
+SET "meetingUrl" = "dailyurl", "meetingPassword" = "dailytoken"
+FROM "DailyEventReference"
+WHERE "DailyEventReference"."bookingId" = "BookingReference"."bookingId" AND "BookingReference"."type" = 'daily_video'
diff --git a/packages/prisma/migrations/20220803091114_drop_daily_event_reference/migration.sql b/packages/prisma/migrations/20220803091114_drop_daily_event_reference/migration.sql
new file mode 100644
index 0000000000..a8947d0810
--- /dev/null
+++ b/packages/prisma/migrations/20220803091114_drop_daily_event_reference/migration.sql
@@ -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";
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 5ebd5b72ee..be8fd2d2f7 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -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)
diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts
index b4b7333eb4..382d09432d 100644
--- a/packages/types/Calendar.d.ts
+++ b/packages/types/Calendar.d.ts
@@ -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;