Fixes/app store keys in db (#2651)
* Adds available apps * Adds App Model * WIP * Updates seeder script * Seeder fixes * lowercase categories * Upgrades prisma * WIP * WIP * Hopefully fixes circular deps * Type fixes * Fixes seeder * Adds migration to connect Credentials to Apps * Updates app store callbacks * Updates google credentials * Uses dirName from DB * Type fixes * Update reschedule.ts * Seeder fixes * Fixes categories listing * Update index.ts * Update schema.prisma * Updates dependencies * Renames giphy app * Uses dynamic imports for app metadata * Fixes credentials error * Uses dynamic import for api handlers * Dynamic import fixes * Allows for simple folder names in app store * Squashes app migrations * seeder fixes * Fixes dyamic imports * Update apiHandlers.tsx
This commit is contained in:
parent
21867c9cd4
commit
6a27fb2959
68 changed files with 995 additions and 1628 deletions
|
@ -117,6 +117,7 @@ EMAIL_SERVER_PASSWORD='<office365_password>'
|
||||||
# **********************************************************************************************************
|
# **********************************************************************************************************
|
||||||
|
|
||||||
# - APP STORE **********************************************************************************************
|
# - APP STORE **********************************************************************************************
|
||||||
|
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||||
# - DAILY.CO VIDEO
|
# - DAILY.CO VIDEO
|
||||||
DAILY_API_KEY=
|
DAILY_API_KEY=
|
||||||
DAILY_SCALE_PLAN=''
|
DAILY_SCALE_PLAN=''
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 943cd10de1f6661273d2ec18acdaa93118852714
|
Subproject commit cf71a8b47ec9d37da7e4facb61356a293cb0bd13
|
2
apps/api
2
apps/api
|
@ -1 +1 @@
|
||||||
Subproject commit 2449d90bcbbf4c0a379f4d766aee299caad488a2
|
Subproject commit 6124577bc21502c018378a299e50fc96bff14b99
|
|
@ -11,15 +11,15 @@ import {
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
ViewGridIcon,
|
ViewGridIcon,
|
||||||
} from "@heroicons/react/solid";
|
} from "@heroicons/react/solid";
|
||||||
|
import { UserPlan } from "@prisma/client";
|
||||||
import { SessionContextValue, signOut, useSession } from "next-auth/react";
|
import { SessionContextValue, signOut, useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { Fragment, ReactNode, useEffect } from "react";
|
import React, { Fragment, ReactNode, useEffect } from "react";
|
||||||
import toast, { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
import { useIsEmbed } from "@calcom/embed-core";
|
import { useIsEmbed } from "@calcom/embed-core";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { UserPlan } from "@calcom/prisma/client";
|
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import Dropdown, {
|
import Dropdown, {
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { Attendee } from "@prisma/client";
|
||||||
import { TFunction } from "next-i18next";
|
import { TFunction } from "next-i18next";
|
||||||
|
|
||||||
import { Attendee } from "@calcom/prisma/client";
|
|
||||||
import { Person } from "@calcom/types/Calendar";
|
import { Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
export const attendeeToPersonConversionType = (attendees: Attendee[], t: TFunction): Person[] => {
|
export const attendeeToPersonConversionType = (attendees: Attendee[], t: TFunction): Person[] => {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import utc from "dayjs/plugin/utc";
|
||||||
import { createEvent, DateArray, Person } from "ics";
|
import { createEvent, DateArray, Person } from "ics";
|
||||||
|
|
||||||
import { getCancelLink } from "@calcom/lib/CalEventParser";
|
import { getCancelLink } from "@calcom/lib/CalEventParser";
|
||||||
import { Attendee } from "@calcom/prisma/client";
|
|
||||||
import { CalendarEvent } from "@calcom/types/Calendar";
|
import { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
import { Prisma } from "@calcom/prisma/client";
|
|
||||||
|
|
||||||
async function getBooking(prisma: PrismaClient, uid: string) {
|
async function getBooking(prisma: PrismaClient, uid: string) {
|
||||||
const booking = await prisma.booking.findFirst({
|
const booking = await prisma.booking.findFirst({
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { useEffect } from "react";
|
import { UserPlan } from "@prisma/client";
|
||||||
|
|
||||||
import { UserPlan } from "@calcom/prisma/client";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: It should be exposed at a single place.
|
* TODO: It should be exposed at a single place.
|
||||||
|
|
|
@ -87,7 +87,7 @@ const nextConfig = {
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
const redirects = [
|
||||||
{
|
{
|
||||||
source: "/settings",
|
source: "/settings",
|
||||||
destination: "/settings/profile",
|
destination: "/settings/profile",
|
||||||
|
@ -104,6 +104,28 @@ const nextConfig = {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
||||||
|
redirects.push(
|
||||||
|
{
|
||||||
|
source: "/apps/dailyvideo",
|
||||||
|
destination: "/apps/daily-video",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/apps/huddle01_video",
|
||||||
|
destination: "/apps/huddle01",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/apps/jitsi_video",
|
||||||
|
destination: "/apps/jitsi",
|
||||||
|
permanent: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirects;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
|
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import appStore from "@calcom/app-store";
|
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
|
@ -19,13 +17,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const appName = _appName.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
|
const appName = _appName.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Find a way to dynamically import these modules
|
/* Absolute path didn't work */
|
||||||
// const app = (await import(`@calcom/${appName}`)).default;
|
const handlerMap = (await import("@calcom/app-store/apiHandlers")).default;
|
||||||
const app = appStore[appName as keyof typeof appStore];
|
const handlers = await handlerMap[appName as keyof typeof handlerMap];
|
||||||
if (!(app && "api" in app && apiEndpoint in app.api))
|
const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler;
|
||||||
throw new HttpError({ statusCode: 404, message: `API handler not found` });
|
|
||||||
|
|
||||||
const handler = app.api[apiEndpoint as keyof typeof app.api] as NextApiHandler;
|
|
||||||
|
|
||||||
if (typeof handler !== "function")
|
if (typeof handler !== "function")
|
||||||
throw new HttpError({ statusCode: 404, message: `API handler not found` });
|
throw new HttpError({ statusCode: 404, message: `API handler not found` });
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import matter from "gray-matter";
|
import matter from "gray-matter";
|
||||||
import { GetStaticPaths, GetStaticPathsResult, GetStaticPropsContext } from "next";
|
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||||
import { MDXRemote } from "next-mdx-remote";
|
import { MDXRemote } from "next-mdx-remote";
|
||||||
import { serialize } from "next-mdx-remote/serialize";
|
import { serialize } from "next-mdx-remote/serialize";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import useMediaQuery from "@lib/hooks/useMediaQuery";
|
import useMediaQuery from "@lib/hooks/useMediaQuery";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
@ -68,11 +69,8 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
||||||
const appStore = getAppRegistry();
|
const appStore = await prisma.app.findMany({ select: { slug: true } });
|
||||||
const paths = appStore.reduce((paths, app) => {
|
const paths = appStore.map(({ slug }) => ({ params: { slug } }));
|
||||||
paths.push({ params: { slug: app.slug } });
|
|
||||||
return paths;
|
|
||||||
}, [] as GetStaticPathsResult<{ slug: string }>["paths"]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths,
|
paths,
|
||||||
|
@ -81,23 +79,19 @@ export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||||
const appStore = getAppRegistry();
|
if (typeof ctx.params?.slug !== "string") return { notFound: true };
|
||||||
|
|
||||||
if (typeof ctx.params?.slug !== "string") {
|
const app = await prisma.app.findUnique({
|
||||||
return {
|
where: { slug: ctx.params.slug },
|
||||||
notFound: true,
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const singleApp = appStore.find((app) => app.slug === ctx.params?.slug);
|
if (!app) return { notFound: true };
|
||||||
|
|
||||||
if (!singleApp) {
|
const singleApp = await getAppWithMetadata(app);
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const appDirname = singleApp.type.replace("_", "");
|
if (!singleApp) return { notFound: true };
|
||||||
|
|
||||||
|
const appDirname = app.dirName;
|
||||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/README.mdx`);
|
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/README.mdx`);
|
||||||
const postFilePath = path.join(README_PATH);
|
const postFilePath = path.join(README_PATH);
|
||||||
let source = "";
|
let source = "";
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default function Apps({ appStore }: InferGetStaticPropsType<typeof getSta
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticPaths = async () => {
|
export const getStaticPaths = async () => {
|
||||||
const appStore = getAppRegistry();
|
const appStore = await getAppRegistry();
|
||||||
const paths = appStore.reduce((categories, app) => {
|
const paths = appStore.reduce((categories, app) => {
|
||||||
if (!categories.includes(app.category)) {
|
if (!categories.includes(app.category)) {
|
||||||
categories.push(app.category);
|
categories.push(app.category);
|
||||||
|
@ -67,7 +67,7 @@ export const getStaticPaths = async () => {
|
||||||
export const getStaticProps = async () => {
|
export const getStaticProps = async () => {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
appStore: getAppRegistry(),
|
appStore: await getAppRegistry(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps = async () => {
|
export const getStaticProps = async () => {
|
||||||
const appStore = getAppRegistry();
|
const appStore = await getAppRegistry();
|
||||||
const categories = appStore.reduce((c, app) => {
|
const categories = appStore.reduce((c, app) => {
|
||||||
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
||||||
return c;
|
return c;
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps = async () => {
|
export const getStaticProps = async () => {
|
||||||
const appStore = getAppRegistry();
|
const appStore = await getAppRegistry();
|
||||||
const categories = appStore.reduce((c, app) => {
|
const categories = appStore.reduce((c, app) => {
|
||||||
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
||||||
return c;
|
return c;
|
||||||
|
|
|
@ -136,26 +136,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
};
|
};
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
async function getBooking() {
|
|
||||||
return prisma.booking.findFirst({
|
|
||||||
where: {
|
|
||||||
uid: asStringOrThrow(context.query.rescheduleUid),
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
description: true,
|
|
||||||
attendees: {
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type Booking = Prisma.PromiseReturnType<typeof getBooking>;
|
|
||||||
let booking: Booking | null = null;
|
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
name: user.name || user.username,
|
name: user.name || user.username,
|
||||||
image: user.avatar,
|
image: user.avatar,
|
||||||
|
@ -173,7 +153,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
locationLabels: getLocationLabels(t),
|
locationLabels: getLocationLabels(t),
|
||||||
profile,
|
profile,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
booking,
|
booking: null,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isDynamicGroupBooking: false,
|
isDynamicGroupBooking: false,
|
||||||
hasHashedBookingLink: true,
|
hasHashedBookingLink: true,
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SelectGifInput } from "@calcom/app-store/giphyother/components";
|
import { SelectGifInput } from "@calcom/app-store/giphy/components";
|
||||||
import getApps, { getLocationOptions, hasIntegration } from "@calcom/app-store/utils";
|
import getApps, { getLocationOptions, hasIntegration } from "@calcom/app-store/utils";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
|
@ -2104,6 +2104,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
type: true,
|
type: true,
|
||||||
key: true,
|
key: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
|
appId: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -708,6 +708,7 @@ export async function getServerSideProps(context: NextPageContext) {
|
||||||
type: true,
|
type: true,
|
||||||
key: true,
|
key: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
|
appId: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { UserPlan } from "@prisma/client";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
import { UserPlan } from "@calcom/prisma/client";
|
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { getWorkingHours } from "@lib/availability";
|
import { getWorkingHours } from "@lib/availability";
|
||||||
import getBooking, { GetBookingType } from "@lib/getBooking";
|
import getBooking, { GetBookingType } from "@lib/getBooking";
|
||||||
|
|
|
@ -55,6 +55,7 @@ async function getUserFromSession({
|
||||||
type: true,
|
type: true,
|
||||||
key: true,
|
key: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
|
appId: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: "asc",
|
id: "asc",
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { expect, it } from "@jest/globals";
|
import { expect, it } from "@jest/globals";
|
||||||
|
import { Availability } from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import MockDate from "mockdate";
|
import MockDate from "mockdate";
|
||||||
|
|
||||||
import { Availability } from "@calcom/prisma/client";
|
|
||||||
|
|
||||||
import { getAvailabilityFromSchedule } from "@lib/availability";
|
import { getAvailabilityFromSchedule } from "@lib/availability";
|
||||||
|
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
|
|
|
@ -1,15 +1,33 @@
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
import { App } from "@calcom/types/App";
|
import { App } from "@calcom/types/App";
|
||||||
|
|
||||||
import appStoreMetadata from "./metadata";
|
export async function getAppWithMetadata(app: { dirName: string }) {
|
||||||
|
let appMetadata: App | null = null;
|
||||||
|
try {
|
||||||
|
appMetadata = (await import(`./${app.dirName}/_metadata`)).default as App;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`No metadata found for: "${app.dirName}". Message:`, error.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!appMetadata) return null;
|
||||||
|
// Let's not leak api keys to the front end
|
||||||
|
const { key, ...metadata } = appMetadata;
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
/** Mainly to use in listings for the frontend, use in getStaticProps or getServerSideProps */
|
/** Mainly to use in listings for the frontend, use in getStaticProps or getServerSideProps */
|
||||||
export function getAppRegistry() {
|
export async function getAppRegistry() {
|
||||||
return Object.values(appStoreMetadata).reduce((apps, app) => {
|
const dbApps = await prisma.app.findMany({ select: { dirName: true, slug: true, categories: true } });
|
||||||
|
const apps = [] as Omit<App, "key">[];
|
||||||
|
for await (const dbapp of dbApps) {
|
||||||
|
const app = await getAppWithMetadata(dbapp);
|
||||||
|
if (!app) continue;
|
||||||
// Skip if app isn't installed
|
// Skip if app isn't installed
|
||||||
if (!app.installed) return apps;
|
/* This is now handled from the DB */
|
||||||
// Let's not leak api keys to the front end
|
// if (!app.installed) return apps;
|
||||||
const { key, ...metadata } = app;
|
apps.push(app);
|
||||||
apps.push(metadata);
|
}
|
||||||
return apps;
|
return apps;
|
||||||
}, [] as Omit<App, "key">[]);
|
|
||||||
}
|
}
|
||||||
|
|
10
packages/app-store/_utils/getAppKeysFromSlug.ts
Normal file
10
packages/app-store/_utils/getAppKeysFromSlug.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
async function getAppKeysFromSlug(slug: string) {
|
||||||
|
const app = await prisma.app.findUnique({ where: { slug } });
|
||||||
|
return app?.keys as Prisma.JsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getAppKeysFromSlug;
|
|
@ -5,7 +5,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { App } from "@calcom/types/App";
|
import { App } from "@calcom/types/App";
|
||||||
|
|
||||||
function useAddAppMutation(type: App["type"], options?: Parameters<typeof useMutation>[2]) {
|
function useAddAppMutation(type: App["type"], options?: Parameters<typeof useMutation>[2]) {
|
||||||
const appName = type.replaceAll("_", "");
|
const appName = type.replace(/_/g, "");
|
||||||
const mutation = useMutation(async () => {
|
const mutation = useMutation(async () => {
|
||||||
const state: IntegrationOAuthCallbackState = {
|
const state: IntegrationOAuthCallbackState = {
|
||||||
returnTo: WEBAPP_URL + "/apps/installed" + location.search,
|
returnTo: WEBAPP_URL + "/apps/installed" + location.search,
|
||||||
|
|
19
packages/app-store/apiHandlers.tsx
Normal file
19
packages/app-store/apiHandlers.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export const apiHandlers = {
|
||||||
|
// examplevideo: import("./_example/api"),
|
||||||
|
applecalendar: import("./applecalendar/api"),
|
||||||
|
caldavcalendar: import("./caldavcalendar/api"),
|
||||||
|
googlecalendar: import("./googlecalendar/api"),
|
||||||
|
hubspotothercalendar: import("./hubspotothercalendar/api"),
|
||||||
|
office365calendar: import("./office365calendar/api"),
|
||||||
|
slackmessaging: import("./slackmessaging/api"),
|
||||||
|
stripepayment: import("./stripepayment/api"),
|
||||||
|
tandemvideo: import("./tandemvideo/api"),
|
||||||
|
zoomvideo: import("@calcom/zoomvideo/api"),
|
||||||
|
office365video: import("@calcom/office365video/api"),
|
||||||
|
wipemycalother: import("./wipemycalother/api"),
|
||||||
|
jitsivideo: import("./jitsivideo/api"),
|
||||||
|
huddle01video: import("./huddle01video/api"),
|
||||||
|
giphy: import("./giphy/api"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default apiHandlers;
|
|
@ -24,6 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
type: "apple_calendar",
|
type: "apple_calendar",
|
||||||
key: symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY!),
|
key: symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY!),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
appId: "apple-calendar",
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
process.env.CALENDSO_ENCRYPTION_KEY!
|
process.env.CALENDSO_ENCRYPTION_KEY!
|
||||||
),
|
),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
appId: "caldav-calendar",
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const InstallAppButtonMap = {
|
||||||
wipemycalother: dynamic(() => import("./wipemycalother/components/InstallAppButton")),
|
wipemycalother: dynamic(() => import("./wipemycalother/components/InstallAppButton")),
|
||||||
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
||||||
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
||||||
giphyother: dynamic(() => import("./giphyother/components/InstallAppButton")),
|
giphy: dynamic(() => import("./giphy/components/InstallAppButton")),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InstallAppButton = (
|
export const InstallAppButton = (
|
||||||
|
@ -33,8 +33,14 @@ export const InstallAppButton = (
|
||||||
) => {
|
) => {
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const appName = props.type.replaceAll("_", "") as keyof typeof InstallAppButtonMap;
|
let appName = props.type.replace(/_/g, "");
|
||||||
const InstallAppButtonComponent = InstallAppButtonMap[appName];
|
let InstallAppButtonComponent = InstallAppButtonMap[appName as keyof typeof InstallAppButtonMap];
|
||||||
|
/** So we can either call it by simple name (ex. `slack`, `giphy`) instead of
|
||||||
|
* `slackmessaging`, `giphyother` while maintaining retro-compatibility. */
|
||||||
|
if (!InstallAppButtonComponent) {
|
||||||
|
[appName] = props.type.split("_");
|
||||||
|
InstallAppButtonComponent = InstallAppButtonMap[appName as keyof typeof InstallAppButtonMap];
|
||||||
|
}
|
||||||
if (!InstallAppButtonComponent) return null;
|
if (!InstallAppButtonComponent) return null;
|
||||||
if (status === "unauthenticated")
|
if (status === "unauthenticated")
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -57,6 +57,7 @@ export const FAKE_DAILY_CREDENTIAL: Credential = {
|
||||||
type: "daily_video",
|
type: "daily_video",
|
||||||
key: { apikey: process.env.DAILY_API_KEY },
|
key: { apikey: process.env.DAILY_API_KEY },
|
||||||
userId: +new Date().getTime(),
|
userId: +new Date().getTime(),
|
||||||
|
appId: "daily-video",
|
||||||
};
|
};
|
||||||
|
|
||||||
const DailyVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
const DailyVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
||||||
|
|
|
@ -8,8 +8,8 @@ export const metadata = {
|
||||||
installed: !!process.env.GIPHY_API_KEY,
|
installed: !!process.env.GIPHY_API_KEY,
|
||||||
category: "other",
|
category: "other",
|
||||||
// If using static next public folder, can then be referenced from the base URL (/).
|
// If using static next public folder, can then be referenced from the base URL (/).
|
||||||
imageSrc: "/api/app-store/giphyother/icon.svg",
|
imageSrc: "/api/app-store/giphy/icon.svg",
|
||||||
logo: "/api/app-store/giphyother/icon.svg",
|
logo: "/api/app-store/giphy/icon.svg",
|
||||||
publisher: "Cal.com",
|
publisher: "Cal.com",
|
||||||
rating: 0,
|
rating: 0,
|
||||||
reviews: 0,
|
reviews: 0,
|
|
@ -2,7 +2,8 @@ import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||||
import { InstallAppButtonProps } from "../../types";
|
import { InstallAppButtonProps } from "../../types";
|
||||||
|
|
||||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||||
const mutation = useAddAppMutation("giphy_other");
|
// @ts-ignore TODO: deprecate App types in favor of DB slugs
|
||||||
|
const mutation = useAddAppMutation("giphy");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
|
@ -30,7 +30,7 @@ export const SearchDialog = (props: ISearchDialog) => {
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
const res = await fetch("/api/integrations/giphyother/search", {
|
const res = await fetch("/api/integrations/giphy/search", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
|
@ -1,6 +1,15 @@
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
|
let api_key = "";
|
||||||
|
|
||||||
export const searchGiphy = async (locale: string, keyword: string, offset: number = 0) => {
|
export const searchGiphy = async (locale: string, keyword: string, offset: number = 0) => {
|
||||||
|
const appKeys = await getAppKeysFromSlug("giphy");
|
||||||
|
if (typeof appKeys.api_key === "string") api_key = appKeys.api_key;
|
||||||
|
if (!api_key) throw new HttpError({ statusCode: 400, message: "Missing Giphy api_key" });
|
||||||
const queryParams = new URLSearchParams({
|
const queryParams = new URLSearchParams({
|
||||||
api_key: String(process.env.GIPHY_API_KEY),
|
api_key,
|
||||||
q: keyword,
|
q: keyword,
|
||||||
limit: "1",
|
limit: "1",
|
||||||
offset: String(offset),
|
offset: String(offset),
|
Before Width: | Height: | Size: 734 B After Width: | Height: | Size: 734 B |
|
@ -4,17 +4,24 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||||
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
const credentials = process.env.GOOGLE_API_CREDENTIALS!;
|
|
||||||
const scopes = [
|
const scopes = [
|
||||||
"https://www.googleapis.com/auth/calendar.readonly",
|
"https://www.googleapis.com/auth/calendar.readonly",
|
||||||
"https://www.googleapis.com/auth/calendar.events",
|
"https://www.googleapis.com/auth/calendar.events",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let client_id = "";
|
||||||
|
let client_secret = "";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
// Get token from Google Calendar API
|
// Get token from Google Calendar API
|
||||||
const { client_secret, client_id } = JSON.parse(credentials).web;
|
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||||
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||||
|
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||||
|
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||||
|
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||||
const redirect_uri = WEBAPP_URL + "/api/integrations/googlecalendar/callback";
|
const redirect_uri = WEBAPP_URL + "/api/integrations/googlecalendar/callback";
|
||||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,10 @@ import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
const credentials = process.env.GOOGLE_API_CREDENTIALS;
|
let client_id = "";
|
||||||
|
let client_secret = "";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { code } = req.query;
|
const { code } = req.query;
|
||||||
|
@ -15,11 +17,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
res.status(400).json({ message: "`code` must be a string" });
|
res.status(400).json({ message: "`code` must be a string" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!credentials) {
|
if (!req.session?.user?.id) {
|
||||||
res.status(400).json({ message: "There are no Google Credentials installed." });
|
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const { client_secret, client_id } = JSON.parse(credentials).web;
|
|
||||||
|
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||||
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||||
|
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||||
|
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||||
|
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||||
|
|
||||||
const redirect_uri = WEBAPP_URL + "/api/integrations/googlecalendar/callback";
|
const redirect_uri = WEBAPP_URL + "/api/integrations/googlecalendar/callback";
|
||||||
|
|
||||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||||
|
@ -36,7 +43,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
data: {
|
data: {
|
||||||
type: "google_calendar",
|
type: "google_calendar",
|
||||||
key,
|
key,
|
||||||
userId: req.session?.user.id,
|
userId: req.session.user.id,
|
||||||
|
appId: "google-calendar",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const state = decodeOAuthState(req);
|
const state = decodeOAuthState(req);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Auth, calendar_v3, google } from "googleapis";
|
||||||
|
|
||||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||||
import CalendarService from "@calcom/lib/CalendarService";
|
import CalendarService from "@calcom/lib/CalendarService";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type {
|
import type {
|
||||||
|
@ -14,26 +15,38 @@ import type {
|
||||||
NewCalendarEventType,
|
NewCalendarEventType,
|
||||||
} from "@calcom/types/Calendar";
|
} from "@calcom/types/Calendar";
|
||||||
|
|
||||||
const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "";
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
export default class GoogleCalendarService implements Calendar {
|
export default class GoogleCalendarService implements Calendar {
|
||||||
private url = "";
|
private url = "";
|
||||||
private integrationName = "";
|
private integrationName = "";
|
||||||
private auth: { getToken: () => Promise<MyGoogleAuth> };
|
private auth: Promise<{ getToken: () => Promise<MyGoogleAuth> }>;
|
||||||
private log: typeof logger;
|
private log: typeof logger;
|
||||||
|
private client_id = "";
|
||||||
|
private client_secret = "";
|
||||||
|
private redirect_uri = "";
|
||||||
|
|
||||||
constructor(credential: Credential) {
|
constructor(credential: Credential) {
|
||||||
this.integrationName = "google_calendar";
|
this.integrationName = "google_calendar";
|
||||||
|
|
||||||
this.auth = this.googleAuth(credential);
|
this.auth = this.googleAuth(credential).then((m) => m);
|
||||||
|
|
||||||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||||
}
|
}
|
||||||
|
|
||||||
private googleAuth = (credential: Credential) => {
|
private googleAuth = async (credential: Credential) => {
|
||||||
const { client_secret, client_id, redirect_uris } = JSON.parse(GOOGLE_API_CREDENTIALS).web;
|
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||||
|
if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id;
|
||||||
|
if (typeof appKeys.client_secret === "string") this.client_secret = appKeys.client_secret;
|
||||||
|
if (typeof appKeys.redirect_uris === "object" && Array.isArray(appKeys.redirect_uris)) {
|
||||||
|
this.redirect_uri = appKeys.redirect_uris[0] as string;
|
||||||
|
}
|
||||||
|
if (!this.client_id) throw new HttpError({ statusCode: 400, message: "Google client_id missing." });
|
||||||
|
if (!this.client_secret)
|
||||||
|
throw new HttpError({ statusCode: 400, message: "Google client_secret missing." });
|
||||||
|
if (!this.redirect_uri) throw new HttpError({ statusCode: 400, message: "Google redirect_uri missing." });
|
||||||
|
|
||||||
const myGoogleAuth = new MyGoogleAuth(client_id, client_secret, redirect_uris[0]);
|
const myGoogleAuth = new MyGoogleAuth(this.client_id, this.client_secret, this.redirect_uri);
|
||||||
|
|
||||||
const googleCredentials = credential.key as Auth.Credentials;
|
const googleCredentials = credential.key as Auth.Credentials;
|
||||||
myGoogleAuth.setCredentials(googleCredentials);
|
myGoogleAuth.setCredentials(googleCredentials);
|
||||||
|
@ -43,23 +56,20 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
const refreshAccessToken = () =>
|
const refreshAccessToken = () =>
|
||||||
myGoogleAuth
|
myGoogleAuth
|
||||||
.refreshToken(googleCredentials.refresh_token)
|
.refreshToken(googleCredentials.refresh_token)
|
||||||
.then((res: GetTokenResponse) => {
|
.then(async (res: GetTokenResponse) => {
|
||||||
const token = res.res?.data;
|
const token = res.res?.data;
|
||||||
googleCredentials.access_token = token.access_token;
|
googleCredentials.access_token = token.access_token;
|
||||||
googleCredentials.expiry_date = token.expiry_date;
|
googleCredentials.expiry_date = token.expiry_date;
|
||||||
return prisma.credential
|
await prisma.credential.update({
|
||||||
.update({
|
where: {
|
||||||
where: {
|
id: credential.id,
|
||||||
id: credential.id,
|
},
|
||||||
},
|
data: {
|
||||||
data: {
|
key: googleCredentials as Prisma.InputJsonValue,
|
||||||
key: googleCredentials as Prisma.InputJsonValue,
|
},
|
||||||
},
|
});
|
||||||
})
|
myGoogleAuth.setCredentials(googleCredentials);
|
||||||
.then(() => {
|
return myGoogleAuth;
|
||||||
myGoogleAuth.setCredentials(googleCredentials);
|
|
||||||
return myGoogleAuth;
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.log.error("Error refreshing google token", err);
|
this.log.error("Error refreshing google token", err);
|
||||||
|
@ -73,164 +83,164 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
};
|
};
|
||||||
|
|
||||||
async createEvent(calEventRaw: CalendarEvent): Promise<NewCalendarEventType> {
|
async createEvent(calEventRaw: CalendarEvent): Promise<NewCalendarEventType> {
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise(async (resolve, reject) => {
|
||||||
this.auth.getToken().then((myGoogleAuth) => {
|
const auth = await this.auth;
|
||||||
const payload: calendar_v3.Schema$Event = {
|
const myGoogleAuth = await auth.getToken();
|
||||||
summary: calEventRaw.title,
|
const payload: calendar_v3.Schema$Event = {
|
||||||
description: getRichDescription(calEventRaw),
|
summary: calEventRaw.title,
|
||||||
start: {
|
description: getRichDescription(calEventRaw),
|
||||||
dateTime: calEventRaw.startTime,
|
start: {
|
||||||
timeZone: calEventRaw.organizer.timeZone,
|
dateTime: calEventRaw.startTime,
|
||||||
},
|
timeZone: calEventRaw.organizer.timeZone,
|
||||||
end: {
|
},
|
||||||
dateTime: calEventRaw.endTime,
|
end: {
|
||||||
timeZone: calEventRaw.organizer.timeZone,
|
dateTime: calEventRaw.endTime,
|
||||||
},
|
timeZone: calEventRaw.organizer.timeZone,
|
||||||
attendees: calEventRaw.attendees.map((attendee) => ({
|
},
|
||||||
...attendee,
|
attendees: calEventRaw.attendees.map((attendee) => ({
|
||||||
responseStatus: "accepted",
|
...attendee,
|
||||||
})),
|
responseStatus: "accepted",
|
||||||
reminders: {
|
})),
|
||||||
useDefault: true,
|
reminders: {
|
||||||
},
|
useDefault: true,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (calEventRaw.location) {
|
if (calEventRaw.location) {
|
||||||
payload["location"] = getLocation(calEventRaw);
|
payload["location"] = getLocation(calEventRaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (calEventRaw.conferenceData && calEventRaw.location === "integrations:google:meet") {
|
if (calEventRaw.conferenceData && calEventRaw.location === "integrations:google:meet") {
|
||||||
payload["conferenceData"] = calEventRaw.conferenceData;
|
payload["conferenceData"] = calEventRaw.conferenceData;
|
||||||
}
|
}
|
||||||
const calendar = google.calendar({
|
const calendar = google.calendar({
|
||||||
version: "v3",
|
version: "v3",
|
||||||
});
|
});
|
||||||
calendar.events.insert(
|
calendar.events.insert(
|
||||||
{
|
{
|
||||||
auth: myGoogleAuth,
|
auth: myGoogleAuth,
|
||||||
|
calendarId: calEventRaw.destinationCalendar?.externalId
|
||||||
|
? calEventRaw.destinationCalendar.externalId
|
||||||
|
: "primary",
|
||||||
|
requestBody: payload,
|
||||||
|
conferenceDataVersion: 1,
|
||||||
|
},
|
||||||
|
function (err, event) {
|
||||||
|
if (err || !event?.data) {
|
||||||
|
console.error("There was an error contacting google calendar service: ", err);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
calendar.events.patch({
|
||||||
|
// Update the same event but this time we know the hangout link
|
||||||
calendarId: calEventRaw.destinationCalendar?.externalId
|
calendarId: calEventRaw.destinationCalendar?.externalId
|
||||||
? calEventRaw.destinationCalendar.externalId
|
? calEventRaw.destinationCalendar.externalId
|
||||||
: "primary",
|
: "primary",
|
||||||
requestBody: payload,
|
auth: myGoogleAuth,
|
||||||
conferenceDataVersion: 1,
|
eventId: event.data.id || "",
|
||||||
},
|
requestBody: {
|
||||||
function (err, event) {
|
description: getRichDescription({
|
||||||
if (err || !event?.data) {
|
...calEventRaw,
|
||||||
console.error("There was an error contacting google calendar service: ", err);
|
additionInformation: { hangoutLink: event.data.hangoutLink || "" },
|
||||||
return reject(err);
|
}),
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
calendar.events.patch({
|
return resolve({
|
||||||
// Update the same event but this time we know the hangout link
|
uid: "",
|
||||||
calendarId: calEventRaw.destinationCalendar?.externalId
|
...event.data,
|
||||||
? calEventRaw.destinationCalendar.externalId
|
id: event.data.id || "",
|
||||||
: "primary",
|
additionalInfo: {
|
||||||
auth: myGoogleAuth,
|
hangoutLink: event.data.hangoutLink || "",
|
||||||
eventId: event.data.id || "",
|
},
|
||||||
requestBody: {
|
type: "google_calendar",
|
||||||
description: getRichDescription({
|
password: "",
|
||||||
...calEventRaw,
|
url: "",
|
||||||
additionInformation: { hangoutLink: event.data.hangoutLink || "" },
|
});
|
||||||
}),
|
}
|
||||||
},
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return resolve({
|
|
||||||
uid: "",
|
|
||||||
...event.data,
|
|
||||||
id: event.data.id || "",
|
|
||||||
additionalInfo: {
|
|
||||||
hangoutLink: event.data.hangoutLink || "",
|
|
||||||
},
|
|
||||||
type: "google_calendar",
|
|
||||||
password: "",
|
|
||||||
url: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise(async (resolve, reject) => {
|
||||||
this.auth.getToken().then((myGoogleAuth) => {
|
const auth = await this.auth;
|
||||||
const payload: calendar_v3.Schema$Event = {
|
const myGoogleAuth = await auth.getToken();
|
||||||
summary: event.title,
|
const payload: calendar_v3.Schema$Event = {
|
||||||
description: getRichDescription(event),
|
summary: event.title,
|
||||||
start: {
|
description: getRichDescription(event),
|
||||||
dateTime: event.startTime,
|
start: {
|
||||||
timeZone: event.organizer.timeZone,
|
dateTime: event.startTime,
|
||||||
},
|
timeZone: event.organizer.timeZone,
|
||||||
end: {
|
},
|
||||||
dateTime: event.endTime,
|
end: {
|
||||||
timeZone: event.organizer.timeZone,
|
dateTime: event.endTime,
|
||||||
},
|
timeZone: event.organizer.timeZone,
|
||||||
attendees: event.attendees,
|
},
|
||||||
reminders: {
|
attendees: event.attendees,
|
||||||
useDefault: true,
|
reminders: {
|
||||||
},
|
useDefault: true,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (event.location) {
|
if (event.location) {
|
||||||
payload["location"] = getLocation(event);
|
payload["location"] = getLocation(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const calendar = google.calendar({
|
const calendar = google.calendar({
|
||||||
version: "v3",
|
version: "v3",
|
||||||
|
auth: myGoogleAuth,
|
||||||
|
});
|
||||||
|
calendar.events.update(
|
||||||
|
{
|
||||||
auth: myGoogleAuth,
|
auth: myGoogleAuth,
|
||||||
});
|
calendarId: event.destinationCalendar?.externalId
|
||||||
calendar.events.update(
|
? event.destinationCalendar.externalId
|
||||||
{
|
: "primary",
|
||||||
auth: myGoogleAuth,
|
eventId: uid,
|
||||||
calendarId: event.destinationCalendar?.externalId
|
sendNotifications: true,
|
||||||
? event.destinationCalendar.externalId
|
sendUpdates: "all",
|
||||||
: "primary",
|
requestBody: payload,
|
||||||
eventId: uid,
|
},
|
||||||
sendNotifications: true,
|
function (err, event) {
|
||||||
sendUpdates: "all",
|
if (err) {
|
||||||
requestBody: payload,
|
console.error("There was an error contacting google calendar service: ", err);
|
||||||
},
|
|
||||||
function (err, event) {
|
|
||||||
if (err) {
|
|
||||||
console.error("There was an error contacting google calendar service: ", err);
|
|
||||||
|
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
|
||||||
return resolve(event?.data);
|
|
||||||
}
|
}
|
||||||
);
|
return resolve(event?.data);
|
||||||
})
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEvent(uid: string, event: CalendarEvent): Promise<void> {
|
async deleteEvent(uid: string, event: CalendarEvent): Promise<void> {
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise(async (resolve, reject) => {
|
||||||
this.auth.getToken().then((myGoogleAuth) => {
|
const auth = await this.auth;
|
||||||
const calendar = google.calendar({
|
const myGoogleAuth = await auth.getToken();
|
||||||
version: "v3",
|
const calendar = google.calendar({
|
||||||
|
version: "v3",
|
||||||
|
auth: myGoogleAuth,
|
||||||
|
});
|
||||||
|
calendar.events.delete(
|
||||||
|
{
|
||||||
auth: myGoogleAuth,
|
auth: myGoogleAuth,
|
||||||
});
|
calendarId: event.destinationCalendar?.externalId
|
||||||
calendar.events.delete(
|
? event.destinationCalendar.externalId
|
||||||
{
|
: "primary",
|
||||||
auth: myGoogleAuth,
|
eventId: uid,
|
||||||
calendarId: event.destinationCalendar?.externalId
|
sendNotifications: true,
|
||||||
? event.destinationCalendar.externalId
|
sendUpdates: "all",
|
||||||
: "primary",
|
},
|
||||||
eventId: uid,
|
function (err, event) {
|
||||||
sendNotifications: true,
|
if (err) {
|
||||||
sendUpdates: "all",
|
console.error("There was an error contacting google calendar service: ", err);
|
||||||
},
|
return reject(err);
|
||||||
function (err, event) {
|
|
||||||
if (err) {
|
|
||||||
console.error("There was an error contacting google calendar service: ", err);
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve(event?.data);
|
|
||||||
}
|
}
|
||||||
);
|
return resolve(event?.data);
|
||||||
})
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailability(
|
async getAvailability(
|
||||||
|
@ -238,96 +248,96 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
dateTo: string,
|
dateTo: string,
|
||||||
selectedCalendars: IntegrationCalendar[]
|
selectedCalendars: IntegrationCalendar[]
|
||||||
): Promise<EventBusyDate[]> {
|
): Promise<EventBusyDate[]> {
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise(async (resolve, reject) => {
|
||||||
this.auth.getToken().then((myGoogleAuth) => {
|
const auth = await this.auth;
|
||||||
const calendar = google.calendar({
|
const myGoogleAuth = await auth.getToken();
|
||||||
version: "v3",
|
const calendar = google.calendar({
|
||||||
auth: myGoogleAuth,
|
version: "v3",
|
||||||
});
|
auth: myGoogleAuth,
|
||||||
const selectedCalendarIds = selectedCalendars
|
});
|
||||||
.filter((e) => e.integration === this.integrationName)
|
const selectedCalendarIds = selectedCalendars
|
||||||
.map((e) => e.externalId);
|
.filter((e) => e.integration === this.integrationName)
|
||||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
.map((e) => e.externalId);
|
||||||
// Only calendars of other integrations selected
|
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||||
resolve([]);
|
// Only calendars of other integrations selected
|
||||||
return;
|
resolve([]);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
(selectedCalendarIds.length === 0
|
(selectedCalendarIds.length === 0
|
||||||
? calendar.calendarList
|
? calendar.calendarList
|
||||||
.list()
|
.list()
|
||||||
.then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
|
.then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
|
||||||
: Promise.resolve(selectedCalendarIds)
|
: Promise.resolve(selectedCalendarIds)
|
||||||
)
|
)
|
||||||
.then((calsIds) => {
|
.then((calsIds) => {
|
||||||
calendar.freebusy.query(
|
calendar.freebusy.query(
|
||||||
{
|
{
|
||||||
requestBody: {
|
requestBody: {
|
||||||
timeMin: dateFrom,
|
timeMin: dateFrom,
|
||||||
timeMax: dateTo,
|
timeMax: dateTo,
|
||||||
items: calsIds.map((id) => ({ id: id })),
|
items: calsIds.map((id) => ({ id: id })),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
(err, apires) => {
|
},
|
||||||
if (err) {
|
(err, apires) => {
|
||||||
reject(err);
|
if (err) {
|
||||||
}
|
reject(err);
|
||||||
let result: Prisma.PromiseReturnType<CalendarService["getAvailability"]> = [];
|
|
||||||
|
|
||||||
if (apires?.data.calendars) {
|
|
||||||
result = Object.values(apires.data.calendars).reduce((c, i) => {
|
|
||||||
i.busy?.forEach((busyTime) => {
|
|
||||||
c.push({
|
|
||||||
start: busyTime.start || "",
|
|
||||||
end: busyTime.end || "",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return c;
|
|
||||||
}, [] as typeof result);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
}
|
}
|
||||||
);
|
let result: Prisma.PromiseReturnType<CalendarService["getAvailability"]> = [];
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
this.log.error("There was an error contacting google calendar service: ", err);
|
|
||||||
|
|
||||||
reject(err);
|
if (apires?.data.calendars) {
|
||||||
});
|
result = Object.values(apires.data.calendars).reduce((c, i) => {
|
||||||
})
|
i.busy?.forEach((busyTime) => {
|
||||||
);
|
c.push({
|
||||||
|
start: busyTime.start || "",
|
||||||
|
end: busyTime.end || "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return c;
|
||||||
|
}, [] as typeof result);
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.log.error("There was an error contacting google calendar service: ", err);
|
||||||
|
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async listCalendars(): Promise<IntegrationCalendar[]> {
|
async listCalendars(): Promise<IntegrationCalendar[]> {
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise(async (resolve, reject) => {
|
||||||
this.auth.getToken().then((myGoogleAuth) => {
|
const auth = await this.auth;
|
||||||
const calendar = google.calendar({
|
const myGoogleAuth = await auth.getToken();
|
||||||
version: "v3",
|
const calendar = google.calendar({
|
||||||
auth: myGoogleAuth,
|
version: "v3",
|
||||||
|
auth: myGoogleAuth,
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.calendarList
|
||||||
|
.list()
|
||||||
|
.then((cals) => {
|
||||||
|
resolve(
|
||||||
|
cals.data.items?.map((cal) => {
|
||||||
|
const calendar: IntegrationCalendar = {
|
||||||
|
externalId: cal.id ?? "No id",
|
||||||
|
integration: this.integrationName,
|
||||||
|
name: cal.summary ?? "No name",
|
||||||
|
primary: cal.primary ?? false,
|
||||||
|
};
|
||||||
|
return calendar;
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.log.error("There was an error contacting google calendar service: ", err);
|
||||||
|
|
||||||
|
reject(err);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
calendar.calendarList
|
|
||||||
.list()
|
|
||||||
.then((cals) => {
|
|
||||||
resolve(
|
|
||||||
cals.data.items?.map((cal) => {
|
|
||||||
const calendar: IntegrationCalendar = {
|
|
||||||
externalId: cal.id ?? "No id",
|
|
||||||
integration: this.integrationName,
|
|
||||||
name: cal.summary ?? "No name",
|
|
||||||
primary: cal.primary ?? false,
|
|
||||||
};
|
|
||||||
return calendar;
|
|
||||||
}) || []
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((err: Error) => {
|
|
||||||
this.log.error("There was an error contacting google calendar service: ", err);
|
|
||||||
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,10 @@ import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
const client_id = process.env.HUBSPOT_CLIENT_ID;
|
let client_id = "";
|
||||||
const client_secret = process.env.HUBSPOT_CLIENT_SECRET;
|
let client_secret = "";
|
||||||
const hubspotClient = new hubspot.Client();
|
const hubspotClient = new hubspot.Client();
|
||||||
|
|
||||||
export type HubspotToken = TokenResponseIF & {
|
export type HubspotToken = TokenResponseIF & {
|
||||||
|
@ -24,15 +25,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!client_id) {
|
if (!req.session?.user?.id) {
|
||||||
res.status(400).json({ message: "HubSpot client id missing." });
|
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!client_secret) {
|
const appKeys = await getAppKeysFromSlug("hubspot");
|
||||||
res.status(400).json({ message: "HubSpot client secret missing." });
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||||
return;
|
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||||
}
|
if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." });
|
||||||
|
if (!client_secret) return res.status(400).json({ message: "HubSpot client secret missing." });
|
||||||
|
|
||||||
const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
|
const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
|
||||||
"authorization_code",
|
"authorization_code",
|
||||||
|
@ -48,7 +49,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
data: {
|
data: {
|
||||||
type: "hubspot_other_calendar",
|
type: "hubspot_other_calendar",
|
||||||
key: hubspotToken as any,
|
key: hubspotToken as any,
|
||||||
userId: req.session?.user.id,
|
userId: req.session.user.id,
|
||||||
|
appId: "hubspot",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
type: appType,
|
type: appType,
|
||||||
key: {},
|
key: {},
|
||||||
userId: req.session.user.id,
|
userId: req.session.user.id,
|
||||||
|
appId: "huddle01",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!installation) {
|
if (!installation) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import * as applecalendar from "./applecalendar";
|
import * as applecalendar from "./applecalendar";
|
||||||
import * as caldavcalendar from "./caldavcalendar";
|
import * as caldavcalendar from "./caldavcalendar";
|
||||||
import * as dailyvideo from "./dailyvideo";
|
import * as dailyvideo from "./dailyvideo";
|
||||||
import * as giphyother from "./giphyother";
|
import * as giphy from "./giphy";
|
||||||
import * as googlecalendar from "./googlecalendar";
|
import * as googlecalendar from "./googlecalendar";
|
||||||
import * as googlevideo from "./googlevideo";
|
import * as googlevideo from "./googlevideo";
|
||||||
import * as hubspotothercalendar from "./hubspotothercalendar";
|
import * as hubspotothercalendar from "./hubspotothercalendar";
|
||||||
|
@ -33,7 +33,7 @@ const appStore = {
|
||||||
tandemvideo,
|
tandemvideo,
|
||||||
zoomvideo,
|
zoomvideo,
|
||||||
wipemycalother,
|
wipemycalother,
|
||||||
giphyother,
|
giphy,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default appStore;
|
export default appStore;
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
type: appType,
|
type: appType,
|
||||||
key: {},
|
key: {},
|
||||||
userId: req.session.user.id,
|
userId: req.session.user.id,
|
||||||
|
appId: "jitsi",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!installation) {
|
if (!installation) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { metadata as applecalendar } from "./applecalendar/_metadata";
|
import { metadata as applecalendar } from "./applecalendar/_metadata";
|
||||||
import { metadata as caldavcalendar } from "./caldavcalendar/_metadata";
|
import { metadata as caldavcalendar } from "./caldavcalendar/_metadata";
|
||||||
import { metadata as dailyvideo } from "./dailyvideo/_metadata";
|
import { metadata as dailyvideo } from "./dailyvideo/_metadata";
|
||||||
import { metadata as giphy } from "./giphyother/_metadata";
|
import { metadata as giphy } from "./giphy/_metadata";
|
||||||
import { metadata as googlecalendar } from "./googlecalendar/_metadata";
|
import { metadata as googlecalendar } from "./googlecalendar/_metadata";
|
||||||
import { metadata as googlevideo } from "./googlevideo/_metadata";
|
import { metadata as googlevideo } from "./googlevideo/_metadata";
|
||||||
import { metadata as hubspotothercalendar } from "./hubspotothercalendar/_metadata";
|
import { metadata as hubspotothercalendar } from "./hubspotothercalendar/_metadata";
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { BASE_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];
|
const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];
|
||||||
|
|
||||||
|
let client_id = "";
|
||||||
|
let client_secret = "";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { code } = req.query;
|
const { code } = req.query;
|
||||||
|
|
||||||
|
@ -16,18 +20,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appKeys = await getAppKeysFromSlug("office365-calendar");
|
||||||
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||||
|
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||||
|
if (!client_id) return res.status(400).json({ message: "Office 365 client_id missing." });
|
||||||
|
if (!client_secret) return res.status(400).json({ message: "Office 365 client_secret missing." });
|
||||||
|
|
||||||
const toUrlEncoded = (payload: Record<string, string>) =>
|
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||||
Object.keys(payload)
|
Object.keys(payload)
|
||||||
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
||||||
.join("&");
|
.join("&");
|
||||||
|
|
||||||
const body = toUrlEncoded({
|
const body = toUrlEncoded({
|
||||||
client_id: process.env.MS_GRAPH_CLIENT_ID!,
|
client_id,
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code,
|
code,
|
||||||
scope: scopes.join(" "),
|
scope: scopes.join(" "),
|
||||||
redirect_uri: BASE_URL + "/api/integrations/office365calendar/callback",
|
redirect_uri: WEBAPP_URL + "/api/integrations/office365calendar/callback",
|
||||||
client_secret: process.env.MS_GRAPH_CLIENT_SECRET!,
|
client_secret,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||||
|
@ -59,6 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
type: "office365_calendar",
|
type: "office365_calendar",
|
||||||
key: responseBody,
|
key: responseBody,
|
||||||
userId: req.session?.user.id,
|
userId: req.session?.user.id,
|
||||||
|
appId: "office365-calendar",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { BASE_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
const scopes = ["OnlineMeetings.ReadWrite"];
|
const scopes = ["OnlineMeetings.ReadWrite"];
|
||||||
|
|
||||||
|
let client_id = "";
|
||||||
|
let client_secret = "";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { code } = req.query;
|
const { code } = req.query;
|
||||||
console.log("🚀 ~ file: callback.ts ~ line 14 ~ handler ~ code", req.query);
|
console.log("🚀 ~ file: callback.ts ~ line 14 ~ handler ~ code", req.query);
|
||||||
|
@ -17,18 +21,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appKeys = await getAppKeysFromSlug("office365-calendar");
|
||||||
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||||
|
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||||
|
if (!client_id) return res.status(400).json({ message: "Office 365 client_id missing." });
|
||||||
|
if (!client_secret) return res.status(400).json({ message: "Office 365 client_secret missing." });
|
||||||
|
|
||||||
const toUrlEncoded = (payload: Record<string, string>) =>
|
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||||
Object.keys(payload)
|
Object.keys(payload)
|
||||||
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
||||||
.join("&");
|
.join("&");
|
||||||
|
|
||||||
const body = toUrlEncoded({
|
const body = toUrlEncoded({
|
||||||
client_id: process.env.MS_GRAPH_CLIENT_ID!,
|
client_id,
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code,
|
code,
|
||||||
scope: scopes.join(" "),
|
scope: scopes.join(" "),
|
||||||
redirect_uri: BASE_URL + "/api/integrations/office365video/callback",
|
redirect_uri: WEBAPP_URL + "/api/integrations/office365video/callback",
|
||||||
client_secret: process.env.MS_GRAPH_CLIENT_SECRET!,
|
client_secret,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||||
|
@ -60,6 +70,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
type: "office365_video",
|
type: "office365_video",
|
||||||
key: responseBody,
|
key: responseBody,
|
||||||
userId: req.session?.user.id,
|
userId: req.session?.user.id,
|
||||||
|
appId: "msteams",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ import { stringify } from "querystring";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
const client_id = process.env.SLACK_CLIENT_ID;
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
|
let client_id = "";
|
||||||
const scopes = ["commands", "users:read", "users:read.email", "chat:write", "chat:write.public"];
|
const scopes = ["commands", "users:read", "users:read.email", "chat:write", "chat:write.public"];
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
@ -12,6 +14,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
|
if (!req.session?.user?.id) {
|
||||||
|
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const appKeys = await getAppKeysFromSlug("slack");
|
||||||
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||||
|
if (!client_id) return res.status(400).json({ message: "Slack client_id missing" });
|
||||||
// Get user
|
// Get user
|
||||||
await prisma.user.findFirst({
|
await prisma.user.findFirst({
|
||||||
rejectOnNotFound: true,
|
rejectOnNotFound: true,
|
||||||
|
|
|
@ -14,6 +14,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!req.session?.user?.id) {
|
||||||
|
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
|
}
|
||||||
|
|
||||||
const response = await stripe.oauth.token({
|
const response = await stripe.oauth.token({
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: code.toString(),
|
code: code.toString(),
|
||||||
|
@ -29,7 +33,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
data: {
|
data: {
|
||||||
type: "stripe_payment",
|
type: "stripe_payment",
|
||||||
key: data as unknown as Prisma.InputJsonObject,
|
key: data as unknown as Prisma.InputJsonObject,
|
||||||
userId: req.session?.user.id,
|
userId: req.session.user.id,
|
||||||
|
appId: "stripe",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => {
|
||||||
}, {} as Record<string, App>);
|
}, {} as Record<string, App>);
|
||||||
|
|
||||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||||
select: { id: true, type: true, key: true, userId: true },
|
select: { id: true, type: true, key: true, userId: true, appId: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
||||||
|
@ -66,6 +66,7 @@ function getApps(userCredentials: CredentialData[]) {
|
||||||
type: appMeta.type,
|
type: appMeta.type,
|
||||||
key: appMeta.key!,
|
key: appMeta.key!,
|
||||||
userId: +new Date().getTime(),
|
userId: +new Date().getTime(),
|
||||||
|
appId: appMeta.slug,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
type: appType,
|
type: appType,
|
||||||
key: {},
|
key: {},
|
||||||
userId: req.session.user.id,
|
userId: req.session.user.id,
|
||||||
|
appId: "wipe-my-cal",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!installation) {
|
if (!installation) {
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
import { Credential } from "@prisma/client";
|
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
import { getUid } from "@calcom/lib/CalEventParser";
|
|
||||||
import logger from "@calcom/lib/logger";
|
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
|
||||||
import type { EventResult } from "@calcom/types/EventManager";
|
|
||||||
|
|
||||||
import { getCalendar } from "../../_utils/getCalendar";
|
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
|
||||||
|
|
||||||
/** TODO: Remove once all references are updated to app-store */
|
|
||||||
export { getCalendar };
|
|
||||||
|
|
||||||
export const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
|
|
||||||
const uid: string = getUid(calEvent);
|
|
||||||
const calendar = getCalendar(credential);
|
|
||||||
let success = true;
|
|
||||||
|
|
||||||
// Check if the disabledNotes flag is set to true
|
|
||||||
if (calEvent.hideCalendarNotes) {
|
|
||||||
calEvent.additionalNotes = "Notes have been hidden by the organizer"; // TODO: i18n this string?
|
|
||||||
}
|
|
||||||
|
|
||||||
const creationResult = calendar
|
|
||||||
? await calendar.createEvent(calEvent).catch((e) => {
|
|
||||||
log.error("createEvent failed", e, calEvent);
|
|
||||||
success = false;
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: credential.type,
|
|
||||||
success,
|
|
||||||
uid,
|
|
||||||
createdEvent: creationResult,
|
|
||||||
originalEvent: calEvent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateEvent = async (
|
|
||||||
credential: Credential,
|
|
||||||
calEvent: CalendarEvent,
|
|
||||||
bookingRefUid: string | null
|
|
||||||
): Promise<EventResult> => {
|
|
||||||
const uid = getUid(calEvent);
|
|
||||||
const calendar = getCalendar(credential);
|
|
||||||
let success = true;
|
|
||||||
|
|
||||||
const updatedResult =
|
|
||||||
calendar && bookingRefUid
|
|
||||||
? await calendar.updateEvent(bookingRefUid, calEvent).catch((e) => {
|
|
||||||
log.error("updateEvent failed", e, calEvent);
|
|
||||||
success = false;
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: credential.type,
|
|
||||||
success,
|
|
||||||
uid,
|
|
||||||
updatedEvent: updatedResult,
|
|
||||||
originalEvent: calEvent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteEvent = (credential: Credential, uid: string, event: CalendarEvent): Promise<unknown> => {
|
|
||||||
const calendar = getCalendar(credential);
|
|
||||||
if (calendar) {
|
|
||||||
return calendar.deleteEvent(uid, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve({});
|
|
||||||
};
|
|
|
@ -1,386 +0,0 @@
|
||||||
import { Credential, DestinationCalendar } from "@prisma/client";
|
|
||||||
import async from "async";
|
|
||||||
import merge from "lodash/merge";
|
|
||||||
import { v5 as uuidv5 } from "uuid";
|
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import type { AdditionInformation, CalendarEvent } from "@calcom/types/Calendar";
|
|
||||||
import type {
|
|
||||||
CreateUpdateResult,
|
|
||||||
EventResult,
|
|
||||||
PartialBooking,
|
|
||||||
PartialReference,
|
|
||||||
} from "@calcom/types/EventManager";
|
|
||||||
import type { VideoCallData } from "@calcom/types/VideoApiAdapter";
|
|
||||||
|
|
||||||
import { LocationType } from "../../locations";
|
|
||||||
import getApps from "../../utils";
|
|
||||||
import { createEvent, updateEvent } from "./calendarManager";
|
|
||||||
import { createMeeting, updateMeeting } from "./videoClient";
|
|
||||||
|
|
||||||
export type Event = AdditionInformation & VideoCallData;
|
|
||||||
|
|
||||||
export const isZoom = (location: string): boolean => {
|
|
||||||
return location === "integrations:zoom";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isDaily = (location: string): boolean => {
|
|
||||||
return location === "integrations:daily";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isHuddle01 = (location: string): boolean => {
|
|
||||||
return location === "integrations:huddle01";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isTandem = (location: string): boolean => {
|
|
||||||
return location === "integrations:tandem";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isTeams = (location: string): boolean => {
|
|
||||||
return location === "integrations:office365_video";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isJitsi = (location: string): boolean => {
|
|
||||||
return location === "integrations:jitsi";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isDedicatedIntegration = (location: string): boolean => {
|
|
||||||
return (
|
|
||||||
isZoom(location) ||
|
|
||||||
isDaily(location) ||
|
|
||||||
isHuddle01(location) ||
|
|
||||||
isTandem(location) ||
|
|
||||||
isJitsi(location) ||
|
|
||||||
isTeams(location)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLocationRequestFromIntegration = (location: string) => {
|
|
||||||
if (
|
|
||||||
/** TODO: Handle this dynamically */
|
|
||||||
location === LocationType.GoogleMeet.valueOf() ||
|
|
||||||
location === LocationType.Zoom.valueOf() ||
|
|
||||||
location === LocationType.Daily.valueOf() ||
|
|
||||||
location === LocationType.Jitsi.valueOf() ||
|
|
||||||
location === LocationType.Huddle01.valueOf() ||
|
|
||||||
location === LocationType.Tandem.valueOf() ||
|
|
||||||
location === LocationType.Teams.valueOf()
|
|
||||||
) {
|
|
||||||
const requestId = uuidv5(location, uuidv5.URL);
|
|
||||||
|
|
||||||
return {
|
|
||||||
conferenceData: {
|
|
||||||
createRequest: {
|
|
||||||
requestId: requestId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
location,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const processLocation = (event: CalendarEvent): CalendarEvent => {
|
|
||||||
// If location is set to an integration location
|
|
||||||
// Build proper transforms for evt object
|
|
||||||
// Extend evt object with those transformations
|
|
||||||
if (event.location?.includes("integration")) {
|
|
||||||
const maybeLocationRequestObject = getLocationRequestFromIntegration(event.location);
|
|
||||||
|
|
||||||
event = merge(event, maybeLocationRequestObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
return event;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EventManagerUser = {
|
|
||||||
credentials: Credential[];
|
|
||||||
destinationCalendar: DestinationCalendar | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class EventManager {
|
|
||||||
calendarCredentials: Credential[];
|
|
||||||
videoCredentials: Credential[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes an array of credentials and initializes a new instance of the EventManager.
|
|
||||||
*
|
|
||||||
* @param user
|
|
||||||
*/
|
|
||||||
constructor(user: EventManagerUser) {
|
|
||||||
const appCredentials = getApps(user.credentials).flatMap((app) => app.credentials);
|
|
||||||
this.calendarCredentials = appCredentials.filter((cred) => cred.type.endsWith("_calendar"));
|
|
||||||
this.videoCredentials = appCredentials.filter((cred) => cred.type.endsWith("_video"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a CalendarEvent and creates all necessary integration entries for it.
|
|
||||||
* When a video integration is chosen as the event's location, a video integration
|
|
||||||
* event will be scheduled for it as well.
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
public async create(event: CalendarEvent): Promise<CreateUpdateResult> {
|
|
||||||
const evt = processLocation(event);
|
|
||||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
|
||||||
|
|
||||||
const results: Array<EventResult> = [];
|
|
||||||
// If and only if event type is a dedicated meeting, create a dedicated video meeting.
|
|
||||||
if (isDedicated) {
|
|
||||||
const result = await this.createVideoEvent(evt);
|
|
||||||
if (result.createdEvent) {
|
|
||||||
evt.videoCallData = result.createdEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the calendar event with the proper video call data
|
|
||||||
results.push(...(await this.createAllCalendarEvents(evt)));
|
|
||||||
|
|
||||||
const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => {
|
|
||||||
return {
|
|
||||||
type: result.type,
|
|
||||||
uid: result.createdEvent?.id.toString() ?? "",
|
|
||||||
meetingId: result.createdEvent?.id.toString(),
|
|
||||||
meetingPassword: result.createdEvent?.password,
|
|
||||||
meetingUrl: result.createdEvent?.url,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
results,
|
|
||||||
referencesToCreate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a calendarEvent and a rescheduleUid and updates the event that has the
|
|
||||||
* given uid using the data delivered in the given CalendarEvent.
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
public async update(
|
|
||||||
event: CalendarEvent,
|
|
||||||
rescheduleUid: string,
|
|
||||||
newBookingId?: number
|
|
||||||
): Promise<CreateUpdateResult> {
|
|
||||||
const evt = processLocation(event);
|
|
||||||
|
|
||||||
if (!rescheduleUid) {
|
|
||||||
throw new Error("You called eventManager.update without an `rescheduleUid`. This should never happen.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get details of existing booking.
|
|
||||||
const booking = await prisma.booking.findFirst({
|
|
||||||
where: {
|
|
||||||
uid: rescheduleUid,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
references: {
|
|
||||||
// NOTE: id field removed from select as we don't require for deletingMany
|
|
||||||
// but was giving error on recreate for reschedule, probably because promise.all() didn't finished
|
|
||||||
select: {
|
|
||||||
type: true,
|
|
||||||
uid: true,
|
|
||||||
meetingId: true,
|
|
||||||
meetingPassword: true,
|
|
||||||
meetingUrl: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destinationCalendar: true,
|
|
||||||
payment: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!booking) {
|
|
||||||
throw new Error("booking not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
|
||||||
const results: Array<EventResult> = [];
|
|
||||||
// If and only if event type is a dedicated meeting, update the dedicated video meeting.
|
|
||||||
if (isDedicated) {
|
|
||||||
const result = await this.updateVideoEvent(evt, booking);
|
|
||||||
const [updatedEvent] = Array.isArray(result.updatedEvent) ? result.updatedEvent : [result.updatedEvent];
|
|
||||||
if (updatedEvent) {
|
|
||||||
evt.videoCallData = updatedEvent;
|
|
||||||
evt.location = updatedEvent.url;
|
|
||||||
}
|
|
||||||
results.push(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update all calendar events.
|
|
||||||
results.push(...(await this.updateAllCalendarEvents(evt, booking)));
|
|
||||||
|
|
||||||
const bookingPayment = booking?.payment;
|
|
||||||
|
|
||||||
// Updating all payment to new
|
|
||||||
if (bookingPayment && newBookingId) {
|
|
||||||
const paymentIds = bookingPayment.map((payment) => payment.id);
|
|
||||||
await prisma.payment.updateMany({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: paymentIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
bookingId: newBookingId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we can delete the old booking and its references.
|
|
||||||
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
|
||||||
where: {
|
|
||||||
bookingId: booking.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const attendeeDeletes = prisma.attendee.deleteMany({
|
|
||||||
where: {
|
|
||||||
bookingId: booking.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bookingDeletes = prisma.booking.delete({
|
|
||||||
where: {
|
|
||||||
id: booking.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for all deletions to be applied.
|
|
||||||
await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
results,
|
|
||||||
referencesToCreate: [...booking.references],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates event entries for all calendar integrations given in the credentials.
|
|
||||||
* When noMail is true, no mails will be sent. This is used when the event is
|
|
||||||
* a video meeting because then the mail containing the video credentials will be
|
|
||||||
* more important than the mails created for these bare calendar events.
|
|
||||||
*
|
|
||||||
* When the optional uid is set, it will be used instead of the auto generated uid.
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @param noMail
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async createAllCalendarEvents(event: CalendarEvent): Promise<Array<EventResult>> {
|
|
||||||
/** Can I use destinationCalendar here? */
|
|
||||||
/* How can I link a DC to a cred? */
|
|
||||||
if (event.destinationCalendar) {
|
|
||||||
const destinationCalendarCredentials = this.calendarCredentials.filter(
|
|
||||||
(c) => c.type === event.destinationCalendar?.integration
|
|
||||||
);
|
|
||||||
return Promise.all(destinationCalendarCredentials.map(async (c) => await createEvent(c, event)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not ideal but, if we don't find a destination calendar,
|
|
||||||
* fallback to the first connected calendar
|
|
||||||
*/
|
|
||||||
const [credential] = this.calendarCredentials;
|
|
||||||
if (!credential) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [await createEvent(credential, event)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks which video integration is needed for the event's location and returns
|
|
||||||
* credentials for that - if existing.
|
|
||||||
* @param event
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
private getVideoCredential(event: CalendarEvent): Credential | undefined {
|
|
||||||
if (!event.location) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const integrationName = event.location.replace("integrations:", "");
|
|
||||||
|
|
||||||
return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a video event entry for the selected integration location.
|
|
||||||
*
|
|
||||||
* When optional uid is set, it will be used instead of the auto generated uid.
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private createVideoEvent(event: CalendarEvent): Promise<EventResult> {
|
|
||||||
const credential = this.getVideoCredential(event);
|
|
||||||
|
|
||||||
if (credential) {
|
|
||||||
return createMeeting(credential, event);
|
|
||||||
} else {
|
|
||||||
return Promise.reject(
|
|
||||||
`No suitable credentials given for the requested integration name:${event.location}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the event entries for all calendar integrations given in the credentials.
|
|
||||||
* When noMail is true, no mails will be sent. This is used when the event is
|
|
||||||
* a video meeting because then the mail containing the video credentials will be
|
|
||||||
* more important than the mails created for these bare calendar events.
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @param booking
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private updateAllCalendarEvents(
|
|
||||||
event: CalendarEvent,
|
|
||||||
booking: PartialBooking
|
|
||||||
): Promise<Array<EventResult>> {
|
|
||||||
return async.mapLimit(this.calendarCredentials, 5, async (credential: Credential) => {
|
|
||||||
const bookingRefUid = booking
|
|
||||||
? booking.references.filter((ref) => ref.type === credential.type)[0]?.uid
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return updateEvent(credential, event, bookingRefUid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a single video event.
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @param booking
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) {
|
|
||||||
const credential = this.getVideoCredential(event);
|
|
||||||
|
|
||||||
if (credential) {
|
|
||||||
const bookingRef = booking ? booking.references.filter((ref) => ref.type === credential.type)[0] : null;
|
|
||||||
return updateMeeting(credential, event, bookingRef);
|
|
||||||
} else {
|
|
||||||
return Promise.reject(
|
|
||||||
`No suitable credentials given for the requested integration name:${event.location}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update event to set a cancelled event placeholder on users calendar
|
|
||||||
* remove if virtual calendar is already done and user availability its read from there
|
|
||||||
* and not only in their calendars
|
|
||||||
* @param event
|
|
||||||
* @param booking
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public async updateAndSetCancelledPlaceholder(event: CalendarEvent, booking: PartialBooking) {
|
|
||||||
await this.updateAllCalendarEvents(event, booking);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { BookingStatus, User, Booking, BookingReference } from "@prisma/client";
|
import { Booking, BookingReference, BookingStatus, User } from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import type { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
|
|
||||||
|
import EventManager from "@calcom/core/EventManager";
|
||||||
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
|
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
|
||||||
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
|
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
|
||||||
|
import { deleteMeeting } from "@calcom/core/videoClient";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
@ -11,8 +13,6 @@ import { Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { getCalendar } from "../../_utils/getCalendar";
|
import { getCalendar } from "../../_utils/getCalendar";
|
||||||
import { sendRequestRescheduleEmail } from "./emailManager";
|
import { sendRequestRescheduleEmail } from "./emailManager";
|
||||||
import EventManager from "./eventManager";
|
|
||||||
import { deleteMeeting } from "./videoClient";
|
|
||||||
|
|
||||||
type PersonAttendeeCommonFields = Pick<User, "id" | "email" | "name" | "locale" | "timeZone" | "username">;
|
type PersonAttendeeCommonFields = Pick<User, "id" | "email" | "name" | "locale" | "timeZone" | "username">;
|
||||||
|
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
// @NOTE: This was copy from core since core uses app/store and creates circular dependency
|
|
||||||
// @TODO: Improve import export on appstore/core
|
|
||||||
import { Credential } from "@prisma/client";
|
|
||||||
import short from "short-uuid";
|
|
||||||
import { v5 as uuidv5 } from "uuid";
|
|
||||||
|
|
||||||
import { getUid } from "@calcom/lib/CalEventParser";
|
|
||||||
import logger from "@calcom/lib/logger";
|
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
|
||||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
|
||||||
import type { VideoApiAdapter, VideoApiAdapterFactory } from "@calcom/types/VideoApiAdapter";
|
|
||||||
|
|
||||||
import appStore from "../../index";
|
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
|
|
||||||
|
|
||||||
const translator = short();
|
|
||||||
|
|
||||||
// factory
|
|
||||||
const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] =>
|
|
||||||
withCredentials.reduce<VideoApiAdapter[]>((acc, cred) => {
|
|
||||||
const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
|
|
||||||
const app = appStore[appName as keyof typeof appStore];
|
|
||||||
if (app && "lib" in app && "VideoApiAdapter" in app.lib) {
|
|
||||||
const makeVideoApiAdapter = app.lib.VideoApiAdapter as VideoApiAdapterFactory;
|
|
||||||
const videoAdapter = makeVideoApiAdapter(cred);
|
|
||||||
acc.push(videoAdapter);
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getBusyVideoTimes = (withCredentials: Credential[]) =>
|
|
||||||
Promise.all(getVideoAdapters(withCredentials).map((c) => c.getAvailability())).then((results) =>
|
|
||||||
results.reduce((acc, availability) => acc.concat(availability), [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const createMeeting = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
|
|
||||||
const uid: string = getUid(calEvent);
|
|
||||||
|
|
||||||
if (!credential) {
|
|
||||||
throw new Error(
|
|
||||||
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoAdapters = getVideoAdapters([credential]);
|
|
||||||
const [firstVideoAdapter] = videoAdapters;
|
|
||||||
const createdMeeting = await firstVideoAdapter.createMeeting(calEvent).catch((e) => {
|
|
||||||
log.error("createMeeting failed", e, calEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!createdMeeting) {
|
|
||||||
return {
|
|
||||||
type: credential.type,
|
|
||||||
success: false,
|
|
||||||
uid,
|
|
||||||
originalEvent: calEvent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: credential.type,
|
|
||||||
success: true,
|
|
||||||
uid,
|
|
||||||
createdEvent: createdMeeting,
|
|
||||||
originalEvent: calEvent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMeeting = async (
|
|
||||||
credential: Credential,
|
|
||||||
calEvent: CalendarEvent,
|
|
||||||
bookingRef: PartialReference | null
|
|
||||||
): Promise<EventResult> => {
|
|
||||||
const uid = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
|
|
||||||
|
|
||||||
let success = true;
|
|
||||||
|
|
||||||
const [firstVideoAdapter] = getVideoAdapters([credential]);
|
|
||||||
const updatedMeeting =
|
|
||||||
credential && bookingRef
|
|
||||||
? await firstVideoAdapter.updateMeeting(bookingRef, calEvent).catch((e) => {
|
|
||||||
log.error("updateMeeting failed", e, calEvent);
|
|
||||||
success = false;
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!updatedMeeting) {
|
|
||||||
return {
|
|
||||||
type: credential.type,
|
|
||||||
success,
|
|
||||||
uid,
|
|
||||||
originalEvent: calEvent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: credential.type,
|
|
||||||
success,
|
|
||||||
uid,
|
|
||||||
updatedEvent: updatedMeeting,
|
|
||||||
originalEvent: calEvent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteMeeting = (credential: Credential, uid: string): Promise<unknown> => {
|
|
||||||
if (credential) {
|
|
||||||
return getVideoAdapters([credential])[0].deleteMeeting(uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve({});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { getBusyVideoTimes, createMeeting, updateMeeting, deleteMeeting };
|
|
|
@ -304,6 +304,7 @@ export default class EventManager {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @fixme potential bug since Google Meet are saved as `integrations:google:meet` and there are no `google:meet` type in our DB */
|
||||||
const integrationName = event.location.replace("integrations:", "");
|
const integrationName = event.location.replace("integrations:", "");
|
||||||
|
|
||||||
return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName));
|
return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName));
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
import type { Availability } from "@prisma/client";
|
||||||
import dayjs, { ConfigType } from "dayjs";
|
import dayjs, { ConfigType } from "dayjs";
|
||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
|
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
import type { Availability } from "@calcom/prisma/client";
|
|
||||||
import type { Schedule, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
import type { Schedule, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "AppCategories" AS ENUM ('calendar', 'messaging', 'other', 'payment', 'video', 'web3');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Credential" ADD COLUMN "appId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "App" (
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"dirName" TEXT NOT NULL,
|
||||||
|
"keys" JSONB,
|
||||||
|
"categories" "AppCategories"[],
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "App_pkey" PRIMARY KEY ("slug")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "App_slug_key" ON "App"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "App_dirName_key" ON "App"("dirName");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Credential" ADD CONSTRAINT "Credential_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App"("slug") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Connects each saved Credential to their respective App
|
||||||
|
UPDATE "Credential" SET "appId" = 'apple-calendar' WHERE "type" = 'apple_calendar';
|
||||||
|
UPDATE "Credential" SET "appId" = 'caldav-calendar' WHERE "type" = 'caldav_calendar';
|
||||||
|
UPDATE "Credential" SET "appId" = 'google-calendar' WHERE "type" = 'google_calendar';
|
||||||
|
UPDATE "Credential" SET "appId" = 'google-meet' WHERE "type" = 'google_video';
|
||||||
|
UPDATE "Credential" SET "appId" = 'office365-calendar' WHERE "type" = 'office365_calendar';
|
||||||
|
UPDATE "Credential" SET "appId" = 'msteams' WHERE "type" = 'office365_video';
|
||||||
|
UPDATE "Credential" SET "appId" = 'dailyvideo' WHERE "type" = 'daily_video';
|
||||||
|
UPDATE "Credential" SET "appId" = 'tandem' WHERE "type" = 'tandem_video';
|
||||||
|
UPDATE "Credential" SET "appId" = 'zoom' WHERE "type" = 'zoom_video';
|
||||||
|
UPDATE "Credential" SET "appId" = 'jitsi' WHERE "type" = 'jitsi_video';
|
||||||
|
UPDATE "Credential" SET "appId" = 'hubspot' WHERE "type" = 'hubspot_other_calendar';
|
||||||
|
UPDATE "Credential" SET "appId" = 'wipe-my-cal' WHERE "type" = 'wipemycal_other';
|
||||||
|
UPDATE "Credential" SET "appId" = 'huddle01' WHERE "type" = 'huddle01_video';
|
||||||
|
UPDATE "Credential" SET "appId" = 'slack' WHERE "type" = 'slack_messaging';
|
||||||
|
UPDATE "Credential" SET "appId" = 'stripe' WHERE "type" = 'stripe_payment';
|
|
@ -13,21 +13,22 @@
|
||||||
"db-setup": "run-s db-up db-deploy db-seed",
|
"db-setup": "run-s db-up db-deploy db-seed",
|
||||||
"db-studio": "yarn prisma studio",
|
"db-studio": "yarn prisma studio",
|
||||||
"db-up": "docker-compose up -d",
|
"db-up": "docker-compose up -d",
|
||||||
"deploy": "yarn prisma migrate deploy",
|
"deploy": "yarn prisma migrate deploy && yarn seed-app-store",
|
||||||
"dx": "yarn db-setup",
|
"dx": "yarn db-setup",
|
||||||
"generate-schemas": "prisma generate && prisma format",
|
"generate-schemas": "prisma generate && prisma format",
|
||||||
"postinstall": "yarn generate-schemas"
|
"postinstall": "yarn generate-schemas",
|
||||||
|
"seed-app-store": "ts-node --transpile-only ./seed-app-store.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prisma": "3.10.0",
|
"prisma": "^3.13.0",
|
||||||
"ts-node": "^10.6.0",
|
"ts-node": "^10.6.0",
|
||||||
"zod": "^3.14.4",
|
"zod": "^3.14.4",
|
||||||
"zod-prisma": "^0.5.4"
|
"zod-prisma": "^0.5.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
"@prisma/client": "^3.12.0"
|
"@prisma/client": "^3.13.0"
|
||||||
},
|
},
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
|
@ -37,6 +38,6 @@
|
||||||
"zod-utils.ts"
|
"zod-utils.ts"
|
||||||
],
|
],
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "ts-node ./seed.ts"
|
"seed": "ts-node --transpile-only ./seed.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,11 +77,13 @@ model EventType {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Credential {
|
model Credential {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
type String
|
type String
|
||||||
key Json
|
key Json
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
userId Int?
|
userId Int?
|
||||||
|
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
|
||||||
|
appId String?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserPlan {
|
enum UserPlan {
|
||||||
|
@ -440,3 +442,26 @@ model Session {
|
||||||
expires DateTime
|
expires DateTime
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AppCategories {
|
||||||
|
calendar
|
||||||
|
messaging
|
||||||
|
other
|
||||||
|
payment
|
||||||
|
video
|
||||||
|
web3
|
||||||
|
}
|
||||||
|
|
||||||
|
model App {
|
||||||
|
// The slug for the app store public page inside `/apps/[slug]`
|
||||||
|
slug String @id @unique
|
||||||
|
// The directory name for `/packages/app-store/[dirName]`
|
||||||
|
dirName String @unique
|
||||||
|
// Needed API Keys
|
||||||
|
keys Json?
|
||||||
|
// One or multiple categories to which this app belongs
|
||||||
|
categories AppCategories[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
credentials Credential[]
|
||||||
|
}
|
||||||
|
|
113
packages/prisma/seed-app-store.ts
Normal file
113
packages/prisma/seed-app-store.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
import prisma from ".";
|
||||||
|
|
||||||
|
require("dotenv").config({ path: "../../.env" });
|
||||||
|
|
||||||
|
async function createApp(
|
||||||
|
slug: Prisma.AppCreateInput["slug"],
|
||||||
|
/** The directory name for `/packages/app-store/[dirName]` */
|
||||||
|
dirName: Prisma.AppCreateInput["dirName"],
|
||||||
|
categories: Prisma.AppCreateInput["categories"],
|
||||||
|
keys?: Prisma.AppCreateInput["keys"]
|
||||||
|
) {
|
||||||
|
await prisma.app.upsert({
|
||||||
|
where: { slug },
|
||||||
|
create: { slug, dirName, categories, keys },
|
||||||
|
update: { dirName, categories, keys },
|
||||||
|
});
|
||||||
|
console.log(`📲 Upserted app: '${slug}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Calendar apps
|
||||||
|
await createApp("apple-calendar", "applecalendar", ["calendar"]);
|
||||||
|
await createApp("caldav-calendar", "caldavcalendar", ["calendar"]);
|
||||||
|
try {
|
||||||
|
const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web;
|
||||||
|
await createApp("google-calendar", "googlecalendar", ["calendar"], {
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
redirect_uris,
|
||||||
|
});
|
||||||
|
await createApp("google-meet", "googlevideo", ["video"], { client_id, client_secret, redirect_uris });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) console.error("Error adding google credentials to DB:", e.message);
|
||||||
|
}
|
||||||
|
if (process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET) {
|
||||||
|
await createApp("office365-calendar", "office365calendar", ["calendar"], {
|
||||||
|
client_id: process.env.MS_GRAPH_CLIENT_ID,
|
||||||
|
client_secret: process.env.MS_GRAPH_CLIENT_SECRET,
|
||||||
|
});
|
||||||
|
await createApp("msteams", "office365video", ["video"]);
|
||||||
|
}
|
||||||
|
// Video apps
|
||||||
|
if (process.env.DAILY_API_KEY) {
|
||||||
|
await createApp("dailyvideo", "dailyvideo", ["video"], {
|
||||||
|
api_key: process.env.DAILY_API_KEY,
|
||||||
|
scale_plan: process.env.DAILY_SCALE_PLAN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (process.env.TANDEM_CLIENT_ID && process.env.TANDEM_CLIENT_SECRET) {
|
||||||
|
await createApp("tandem", "tandemvideo", ["video"], {
|
||||||
|
client_id: process.env.TANDEM_CLIENT_ID as string,
|
||||||
|
client_secret: process.env.TANDEM_CLIENT_SECRET as string,
|
||||||
|
base_url: (process.env.TANDEM_BASE_URL as string) || "https://tandem.chat",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET) {
|
||||||
|
await createApp("zoom", "zoomvideo", ["video"], {
|
||||||
|
client_id: process.env.ZOOM_CLIENT_ID,
|
||||||
|
client_secret: process.env.ZOOM_CLIENT_SECRET,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createApp("jitsi", "jitsivideo", ["video"]);
|
||||||
|
// Other apps
|
||||||
|
if (process.env.HUBSPOT_CLIENT_ID && process.env.HUBSPOT_CLIENT_SECRET) {
|
||||||
|
await createApp("hubspot", "hubspotothercalendar", ["other"], {
|
||||||
|
client_id: process.env.HUBSPOT_CLIENT_ID,
|
||||||
|
client_secret: process.env.HUBSPOT_CLIENT_SECRET,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createApp("wipe-my-cal", "wipemycalother", ["other"]);
|
||||||
|
if (process.env.GIPHY_API_KEY) {
|
||||||
|
await createApp("giphy", "giphy", ["other"], {
|
||||||
|
api_key: process.env.GIPHY_API_KEY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Web3 apps
|
||||||
|
await createApp("huddle01", "huddle01video", ["web3", "video"]);
|
||||||
|
// Messaging apps
|
||||||
|
if (process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET && process.env.SLACK_SIGNING_SECRET) {
|
||||||
|
await createApp("slack", "slackmessaging", ["messaging"], {
|
||||||
|
client_id: process.env.SLACK_CLIENT_ID,
|
||||||
|
client_secret: process.env.SLACK_CLIENT_SECRET,
|
||||||
|
signing_secret: process.env.SLACK_SIGNING_SECRET,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Payment apps
|
||||||
|
if (
|
||||||
|
process.env.STRIPE_CLIENT_ID &&
|
||||||
|
process.env.STRIPE_PRIVATE_KEY &&
|
||||||
|
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
||||||
|
process.env.STRIPE_WEBHOOK_SECRET
|
||||||
|
) {
|
||||||
|
await createApp("stripe", "stripepayment", ["payment"], {
|
||||||
|
client_id: process.env.STRIPE_CLIENT_ID,
|
||||||
|
client_secret: process.env.STRIPE_PRIVATE_KEY,
|
||||||
|
payment_fee_fixed: 10,
|
||||||
|
payment_fee_percentage: 0.005,
|
||||||
|
public_key: process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY,
|
||||||
|
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
|
@ -1,13 +1,14 @@
|
||||||
import { MembershipRole, Prisma, PrismaClient, UserPlan } from "@prisma/client";
|
import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { uuid } from "short-uuid";
|
import { uuid } from "short-uuid";
|
||||||
|
|
||||||
import { hashPassword } from "@calcom/lib/auth";
|
import { hashPassword } from "@calcom/lib/auth";
|
||||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||||
|
|
||||||
require("dotenv").config({ path: "../../.env" });
|
import prisma from ".";
|
||||||
|
import "./seed-app-store";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
require("dotenv").config({ path: "../../.env" });
|
||||||
|
|
||||||
async function createUserAndEventType(opts: {
|
async function createUserAndEventType(opts: {
|
||||||
user: {
|
user: {
|
||||||
|
|
0
test
0
test
Loading…
Reference in a new issue