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:
1 parent
14f0c30584
commit
6d79f80928
17 files changed
+426
-370
No files matched your search
@@ -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,
|
||||
};
|
||||
};
|
||||
+4
@@ -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)
|
||||
|
||||
Vendored
+2
-1
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user