pull/2022/head
zomars 2022-02-21 16:14:46 -07:00
parent ed40b09430
commit 355737a86f
30 changed files with 267 additions and 118 deletions

View File

@ -0,0 +1,17 @@
{
"private": true,
"name": "zoom",
"label": "Zoom",
"⬇️ needs to be the same as the folder name": 1,
"slug": "zoom",
"category": "Video Conferencing",
"description": "Zoom is the most popular video conferencing platform, joinable on the web or via desktop/mobile apps.",
"logo": "/apps/zoom.svg",
"publisher": "Cal.com",
"url": "https://zoom.us/",
"verified": true,
"⬇️ TODO: placeholder for now, pull this from TrustPilot or G2": 1,
"rating": 4.3,
"⬇️ TODO 2: placeholder for now, pull this from TrustPilot or G2": 1,
"reviews": 69
}

View File

@ -1,15 +0,0 @@
{
"index": "Home",
"self-hosting": "Self-hosting",
"availability": "Availability",
"bookings": "Bookings",
"event-types": "Event Types",
"teams": "Teams",
"integrations": "Integrations",
"webhooks": "Webhooks",
"settings": "Settings",
"import": "Import",
"billing": "Billing",
"developer": "Developer",
"faq": "FAQs"
}

View File

@ -1,13 +1,11 @@
import { DestinationCalendar, SelectedCalendar } from "@prisma/client";
import { SelectedCalendar } from "@prisma/client";
import { PaymentInfo } from "@ee/lib/stripe/server";
import type { CalendarEvent, ConferenceData } from "@calcom/types/CalendarEvent";
import type { Event } from "@lib/events/EventManager";
import { Ensure } from "@lib/types/utils";
import { VideoCallData } from "@lib/videoClient";
import { ConferenceData } from "../../google_calendar/interfaces/GoogleCalendar";
import { NewCalendarEventType, Person } from "../types/CalendarTypes";
import { NewCalendarEventType } from "../types/CalendarTypes";
export interface EntryPoint {
entryPointType?: string;
@ -26,29 +24,6 @@ export interface AdditionInformation {
hangoutLink?: string;
}
export interface CalendarEvent {
type: string;
title: string;
startTime: string;
endTime: string;
description?: string | null;
team?: {
name: string;
members: string[];
};
location?: string | null;
organizer: Person;
attendees: Person[];
conferenceData?: ConferenceData;
additionInformation?: AdditionInformation;
uid?: string | null;
videoCallData?: VideoCallData;
paymentInfo?: PaymentInfo | null;
destinationCalendar?: DestinationCalendar | null;
cancellationReason?: string | null;
rejectionReason?: string | null;
}
export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> {
primary?: boolean;
name?: string;

View File

@ -1,5 +0,0 @@
import { calendar_v3 } from "googleapis";
export interface ConferenceData {
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
}

View File

@ -0,0 +1,14 @@
{
"index": 1,
"self-hosting": 1,
"availability": 1,
"bookings": 1,
"event-types": 1,
"teams": 1,
"integrations": 1,
"webhooks": 1,
"settings": 1,
"import": 1,
"billing": 1,
"developer": 1
}

View File

@ -1,32 +1,2 @@
import { Prisma } from "@prisma/client";
export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: number; code?: string } {
if (cause instanceof Prisma.PrismaClientKnownRequestError) {
return cause;
}
if (cause instanceof Error) {
return cause;
}
if (typeof cause === "string") {
// @ts-expect-error https://github.com/tc39/proposal-error-cause
return new Error(cause, { cause });
}
return new Error(`Unhandled error of type '${typeof cause}''`);
}
export function handleErrorsJson(response: Response) {
if (!response.ok) {
response.json().then(console.log);
throw Error(response.statusText);
}
return response.json();
}
export function handleErrorsRaw(response: Response) {
if (!response.ok) {
response.text().then(console.log);
throw Error(response.statusText);
}
return response.text();
}
// TODO: Remove this file once everything is imported from `@calcom/lib`
export * from "@calcom/lib/errors";

View File

@ -2,48 +2,36 @@ import { Credential } from "@prisma/client";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import appStore from "@calcom/app-store";
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
import type { PartialReference } from "@calcom/types/EventManager";
import type { VideoApiAdapter, VideoApiAdapterFactory } from "@calcom/types/VideoApiAdapter";
import { getUid } from "@lib/CalEventParser";
import { EventResult } from "@lib/events/EventManager";
import { PartialReference } from "@lib/events/EventManager";
import Huddle01VideoApiAdapter from "@lib/integrations/Huddle01/Huddle01VideoApiAdapter";
import JitsiVideoApiAdapter from "@lib/integrations/Jitsi/JitsiVideoApiAdapter";
import logger from "@lib/logger";
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
import TandemVideoApiAdapter from "./integrations/Tandem/TandemVideoApiAdapter";
import ZoomVideoApiAdapter from "./integrations/Zoom/ZoomVideoApiAdapter";
import { CalendarEvent } from "./integrations/calendar/interfaces/Calendar";
const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
const translator = short();
export interface VideoCallData {
type: string;
id: string;
password: string;
url: string;
}
type EventBusyDate = Record<"start" | "end", Date>;
export interface VideoApiAdapter {
createMeeting(event: CalendarEvent): Promise<VideoCallData>;
updateMeeting(bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData>;
deleteMeeting(uid: string): Promise<unknown>;
getAvailability(dateFrom?: string, dateTo?: string): Promise<EventBusyDate[]>;
}
// factory
const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] =>
withCredentials.reduce<VideoApiAdapter[]>((acc, cred) => {
const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
const makeVideoApiAdapter = appStore[appName].lib?.VideoApiAdapter as VideoApiAdapterFactory;
if (typeof makeVideoApiAdapter !== "undefined") {
const videoAdapter = makeVideoApiAdapter(cred);
acc.push(videoAdapter);
return acc;
}
switch (cred.type) {
case "zoom_video":
acc.push(ZoomVideoApiAdapter(cred));
break;
case "daily_video":
acc.push(DailyVideoApiAdapter(cred));
break;

View File

@ -1,4 +1,5 @@
const withTM = require("@vercel/edge-functions-ui/transpile")([
"@calcom/app-store",
"@calcom/lib",
"@calcom/prisma",
"@calcom/ui",

View File

@ -26,6 +26,7 @@
},
"dependencies": {
"@boxyhq/saml-jackson": "0.3.6",
"@calcom/app-store": "*",
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@calcom/tsconfig": "*",

View File

@ -0,0 +1,45 @@
import { NextApiRequest, NextApiResponse } from "next";
import appStore from "@calcom/app-store";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Check that user is authenticated
req.session = await getSession({ req });
if (!req.session?.user?.id) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
const { args } = req.query;
if (!Array.isArray(args)) {
return res.status(404).json({ message: `API route not found` });
}
const [appName, apiEndpoint] = args;
try {
// TODO: Find a way to dynamically import these modules
// const app = (await import(`@calcom/${appName}`)).default;
const handler = appStore[appName].api[apiEndpoint];
if (typeof handler !== "function")
throw new HttpError({ statusCode: 404, message: `API handler not found` });
const response = await handler(req, res);
console.log("response", response);
res.status(200);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
return res.status(error.statusCode).json({ message: error.message });
}
return res.status(404).json({ message: `API handler not found` });
}
};
export default handler;

View File

@ -1,4 +1,32 @@
// TODO: maybe we wanna do this dynamically later based on folder structure
import fs from "fs";
import path from "path";
// It won't be called on client-side.
export async function getStaticProps() {
const appStoreDir = path.join(process.cwd(), "packages/appStore");
const filenames = fs.readdirSync(appStoreDir);
const apps = filenames.map((filename) => {
const filePath = path.join(appStoreDir, filename);
const fileContents = fs.readFileSync(filePath, "utf8");
// Generally you would parse/transform the contents
// For example you can transform markdown to HTML here
return {
filename,
content: fileContents,
};
});
// By returning { props: posts }, the Blog component
// will receive `posts` as a prop at build time
return {
props: {
posts: apps,
},
};
}
export function appRegistry() {
return [
{

View File

@ -5,7 +5,7 @@
"workspaces": [
"apps/*",
"packages/*",
"appStore/*"
"packages/app-store/*"
],
"scripts": {
"build": "turbo run build --scope=\"@calcom/web\" --include-dependencies",

View File

@ -0,0 +1,17 @@
{
"private": true,
"name": "zoom",
"label": "Zoom",
"⬇️ needs to be the same as the folder name": 1,
"slug": "zoom",
"category": "Video Conferencing",
"description": "Zoom is the most popular video conferencing platform, joinable on the web or via desktop/mobile apps.",
"logo": "/apps/zoom.svg",
"publisher": "Cal.com",
"url": "https://zoom.us/",
"verified": true,
"⬇️ TODO: placeholder for now, pull this from TrustPilot or G2": 1,
"rating": 4.3,
"⬇️ TODO 2: placeholder for now, pull this from TrustPilot or G2": 1,
"reviews": 69
}

View File

@ -0,0 +1,7 @@
import * as zoomvideo from "./zoomvideo";
const appStore = {
zoomvideo,
};
export default appStore;

View File

@ -0,0 +1,8 @@
{
"name": "@calcom/app-store",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/zoomvideo": "*"
}
}

View File

@ -0,0 +1,5 @@
{
"extends": "@calcom/tsconfig/base.json",
"include": [".", "@calcom/types"],
"exclude": ["dist", "build", "node_modules"]
}

View File

@ -2,7 +2,6 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { stringify } from "querystring";
import prisma from "@calcom/prisma";
import "@calcom/types/next";
const BASE_URL = process.env.BASE_URL || `https://${process.env.VERCEL_URL}`;

View File

@ -1,7 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import "@calcom/types/next";
const BASE_URL = process.env.BASE_URL || `https://${process.env.VERCEL_URL}`;

View File

@ -0,0 +1,2 @@
export { default as add } from "./add";
export { default as callback } from "./callback";

View File

@ -0,0 +1,2 @@
export * as api from "./api";
export * as lib from "./lib";

View File

@ -1,11 +1,10 @@
import { Credential } from "@prisma/client";
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
import { PartialReference } from "@lib/events/EventManager";
import prisma from "@lib/prisma";
import { VideoApiAdapter, VideoCallData } from "@lib/videoClient";
import { CalendarEvent } from "../calendar/interfaces/Calendar";
import { handleErrorsJson, handleErrorsRaw } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
import type { PartialReference } from "@calcom/types/EventManager";
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
/** @link https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate */
export interface ZoomEventResult {

View File

@ -0,0 +1 @@
export { default as VideoApiAdapter } from "./VideoApiAdapter";

View File

@ -16,6 +16,9 @@
"⬇️ TODO 2: placeholder for now, pull this from TrustPilot or G2": 1,
"reviews": 69,
"dependencies": {
"@calcom/prisma": "*"
},
"devDependencies": {
"@calcom/types": "*"
}
}

32
packages/lib/errors.ts Normal file
View File

@ -0,0 +1,32 @@
import { Prisma } from "@prisma/client";
export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: number; code?: string } {
if (cause instanceof Prisma.PrismaClientKnownRequestError) {
return cause;
}
if (cause instanceof Error) {
return cause;
}
if (typeof cause === "string") {
// @ts-expect-error https://github.com/tc39/proposal-error-cause
return new Error(cause, { cause });
}
return new Error(`Unhandled error of type '${typeof cause}''`);
}
export function handleErrorsJson(response: Response) {
if (!response.ok) {
response.json().then(console.log);
throw Error(response.statusText);
}
return response.json();
}
export function handleErrorsRaw(response: Response) {
if (!response.ok) {
response.text().then(console.log);
throw Error(response.statusText);
}
return response.text();
}

35
packages/types/CalendarEvent.d.ts vendored Normal file
View File

@ -0,0 +1,35 @@
import type { DestinationCalendar } from "@prisma/client";
import type { calendar_v3 } from "googleapis";
export interface ConferenceData {
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
}
export interface AdditionInformation {
conferenceData?: ConferenceData;
entryPoints?: EntryPoint[];
hangoutLink?: string;
}
export interface CalendarEvent {
type: string;
title: string;
startTime: string;
endTime: string;
description?: string | null;
team?: {
name: string;
members: string[];
};
location?: string | null;
organizer: Person;
attendees: Person[];
conferenceData?: ConferenceData;
additionInformation?: AdditionInformation;
uid?: string | null;
videoCallData?: VideoCallData;
paymentInfo?: PaymentInfo | null;
destinationCalendar?: DestinationCalendar | null;
cancellationReason?: string | null;
rejectionReason?: string | null;
}

22
packages/types/VideoApiAdapter.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
import type { Credential } from "@prisma/client";
export interface VideoCallData {
type: string;
id: string;
password: string;
url: string;
}
export type EventBusyDate = Record<"start" | "end", Date>;
export interface VideoApiAdapter {
createMeeting(event: CalendarEvent): Promise<VideoCallData>;
updateMeeting(bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData>;
deleteMeeting(uid: string): Promise<unknown>;
getAvailability(dateFrom?: string, dateTo?: string): Promise<EventBusyDate[]>;
}
export type VideoApiAdapterFactory = (credential: Credential) => VideoApiAdapter;

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { IncomingMessage } from "http";
import { Session } from "next-auth";
import type { Session } from "next-auth";
import "./next-auth";

View File

@ -1,5 +1,5 @@
{
"extends": "@calcom/tsconfig/base.json",
"include": ["", "@calcom/types"],
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}